Merge branch 'development' into 'master'

chore: release 0.5.0

See merge request nofusscomputing/projects/django_template!21
This commit is contained in:
2024-06-17 15:03:03 +00:00
218 changed files with 7876 additions and 8649 deletions

2
.gitignore vendored
View File

@ -6,3 +6,5 @@ __pycache__
artifacts/
**.tmp.*
volumes/
build/
pages/

View File

@ -73,6 +73,12 @@ Docker Container:
# - '{dockerfile,dockerfile.j2}'
# when: always
- if:
$CI_COMMIT_AUTHOR =='nfc_bot <helpdesk@nofusscomputing.com>'
&&
$CI_COMMIT_BRANCH == "development"
when: never
- if: # condition_not_master_or_dev_push
$CI_COMMIT_BRANCH != "master" &&
$CI_COMMIT_BRANCH != "development" &&

View File

@ -0,0 +1,35 @@
### :books: Summary
<!-- your summary here emojis ref: https://github.com/yodamad/gitlab-emoji -->
### :link: Links / References
<!--
using a list as any links to other references or links as required. if relevent, describe the link/reference
Include any issues or related merge requests. Note: dependent MR's also to be added to "Merge request dependencies"
-->
### :construction_worker: Tasks
- [ ] Add your tasks here if required (delete)
<!-- dont remove tasks below strike through including the checkbox by enclosing in double tidle '~~' -->
- [ ] ~"breaking-change" Any Breaking change(s)
_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/)._
- [ ] ~Documentation Documentation written
_All features to be documented within the correct section(s). Administration, Development and/or User_
- [ ] Milestone assigned
- [ ] [Unit Test(s) Written](https://nofusscomputing.com/projects/django-template/development/testing/)
_ensure test coverage delta is not less than zero_

View File

@ -5,8 +5,12 @@
"!python"
],
"python.testing.pytestArgs": [
// "-v",
// "--cov",
// "--cov-report xml",
"app"
],
"python.testing.unittestEnabled": true,
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"testing.coverageToolbarEnabled": true,
}

View File

@ -41,46 +41,7 @@ Updates to python modules will need to be captured with SCM. This can be done by
!!! danger "Requirement"
All models **are** to have tests written for them, Including testing between dependent models.
To ensure consistency and reliability of this application, tests are to be written. Each test is to test one item ONLY and no more. Each module is to contain a tests directory of the model being tested with a single file for grouping of what is being tested. for items that depend upon a parent model, the test file is to be within the child-models test directory named with format `test_<model>_<parent app>_<parent model name>`
_example structure for the device model that relies upon access app model organization, core app model history and model notes._
``` text
├── tests
│   ├── device
│   │   ├── test_device_access_organization.py
│   │   ├── test_device_api_permission.py
│   │   ├── test_device_core_history.py
│   │   ├── test_device_core_notes.py
│   │   ├── test_device_permission.py
│   │   └── test_device.py
```
Items to test include but are not limited to:
- CRUD permissions admin site
- CRUD permissions api site
- CRUD permissions main site
- can only access organization object
- can access global object (still to require model CRUD permission)
- parent models
### Running Tests
test can be run by running the following:
1. `pip install -r requirements_test.txt -r requirements.txt`
1. `pytest --cov --cov-report html --cov=./`
See [Documentation](https://nofusscomputing.com/projects/django-template/development/testing/) for further information
## Docker Container

View File

@ -6,3 +6,16 @@
![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)
artifacts
dont work to file
https://gitlab.com/nofusscomputing/projects/django_template/-/jobs/artifacts/master/browse/artifacts/coverage/index.html?job=Unit
works to dir
https://gitlab.com/nofusscomputing/projects/django_template/-/jobs/artifacts/master/browse/artifacts/coverage/?job=Unit

View File

@ -17,7 +17,7 @@ class TeamInline(admin.TabularInline):
class OrganizationAdmin(admin.ModelAdmin):
fieldsets = [
(None, {"fields": ["name", "slug"]}),
(None, {"fields": ["name", 'manager', "slug"]}),
#("Date information", {"fields": ["slug"], "classes": ["collapse"]}),
]
inlines = [TeamInline]

View File

@ -0,0 +1,37 @@
from django import forms
from django.db.models import Q
from app import settings
from access.models import Organization
class OrganizationForm(forms.ModelForm):
class Meta:
model = Organization
fields = [
'name',
'manager',
'model_notes',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['created'] = forms.DateTimeField(
label="Created",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].created,
disabled=True,
required=False,
)
self.fields['modified'] = forms.DateTimeField(
label="Modified",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].modified,
disabled=True,
required=False,
)

81
app/access/forms/team.py Normal file
View File

@ -0,0 +1,81 @@
from django import forms
from django.contrib.auth.models import Permission
from django.db.models import Q
from django.forms import inlineformset_factory
from app import settings
from .team_users import TeamUsersForm, TeamUsers
from access.models import Team
TeamUserFormSet = inlineformset_factory(
model=TeamUsers,
parent_model= Team,
extra = 1,
fields=[
'user',
'manager'
]
)
class TeamForm(forms.ModelForm):
class Meta:
model = Team
fields = [
'name',
'permissions',
'model_notes',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['created'] = forms.DateTimeField(
label="Created",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].created,
disabled=True,
required=False,
)
self.fields['modified'] = forms.DateTimeField(
label="Modified",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].modified,
disabled=True,
required=False,
)
self.fields['permissions'].widget.attrs = {'style': "height: 200px;"}
apps = [
'access',
'config_management',
'core',
'itam',
'settings',
]
exclude_models = [
'appsettings',
'organization'
'settings',
'usersettings',
]
exclude_permissions = [
'add_organization',
'change_organization',
'delete_organization',
]
self.fields['permissions'].queryset = Permission.objects.filter(
content_type__app_label__in=apps,
).exclude(
content_type__model__in=exclude_models
).exclude(
codename__in = exclude_permissions
)

View File

@ -0,0 +1,16 @@
from django import forms
from django.db.models import Q
from app import settings
from access.models import TeamUsers
class TeamUsersForm(forms.ModelForm):
class Meta:
model = TeamUsers
fields = [
'user',
'manager',
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2024-06-11 20:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0003_alter_team_organization'),
]
operations = [
migrations.AddField(
model_name='team',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 5.0.6 on 2024-06-17 10:03
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0004_team_model_notes'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='organization',
name='manager',
field=models.ForeignKey(help_text='Organization Manager', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='organization',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
]

View File

@ -6,7 +6,7 @@ from django.utils.functional import cached_property
from .models import Team
from .models import Organization, Team
class OrganizationMixin():
@ -16,6 +16,21 @@ class OrganizationMixin():
user_groups = []
def get_parent_obj(self):
""" Get the Parent Model Object
Use in views where the the model has no organization and the organization should be fetched from the parent model.
Requires attribute `parent_model` within the view with the value of the parent's model class
Returns:
parent_model (Model): with PK from kwargs['pk']
"""
return self.parent_model.objects.get(pk=self.kwargs['pk'])
def object_organization(self) -> int:
id = None
@ -26,7 +41,17 @@ class OrganizationMixin():
self.get_queryset()
if hasattr(self, 'get_object'):
if hasattr(self, 'parent_model'):
obj = self.get_parent_obj()
id = obj.get_organization().id
if obj.is_global:
id = 0
if hasattr(self, 'get_object') and id is None:
obj = self.get_object()
@ -45,6 +70,7 @@ class OrganizationMixin():
id = int(self.request.POST.get("organization", ""))
return id
@ -101,11 +127,8 @@ class OrganizationMixin():
Get All groups the user is part of, fetch the associated team,
iterate over the results adding the organization ID to a list to be returned.
Args:
request (_type_): Current http request
Returns:
_type_: _description_
_type_: User Organizations.
"""
user_organizations = []
@ -124,7 +147,7 @@ class OrganizationMixin():
# 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
@ -152,20 +175,108 @@ class OrganizationMixin():
return has_permission
def permission_check(self, request, permissions_required: list = None) -> bool:
class OrganizationPermission(AccessMixin, OrganizationMixin):
"""checking organization membership"""
def dispatch(self, request, *args, **kwargs):
self.request = request
if not request.user.is_authenticated:
return self.handle_no_permission()
if permissions_required:
self.permission_required = permissions_required
organization_manager_models = [
'access.organization',
'access.team',
'access.teamusers',
]
is_organization_manager = False
if hasattr(self, 'get_object'):
if not self.has_organization_permission() and not request.user.is_superuser:
raise PermissionDenied('You are not part of this organization')
if hasattr(self, 'model'):
if self.model._meta.label_lower in organization_manager_models:
organization = Organization.objects.get(pk=self.object_organization())
if organization.manager == request.user:
is_organization_manager = True
if not self.has_organization_permission() and not request.user.is_superuser and not is_organization_manager:
return False
return True
class OrganizationPermission(AccessMixin, OrganizationMixin):
"""## Permission Checking
The base django permissions have not been modified with this app providing Multi-Tenancy. This is done by a mixin, that checks if the item is apart of an organization, if it is; confirmation is made that the user is part of the same organization and as long as they have the correct permission within the organization, access is granted.
### How it works
The overall permissions system of django has not been modified with it remaining fully functional. The multi-tenancy has been setup based off of an organization with teams. A team to the underlying django system is an extension of the django auth group and for every team created a django auth group is created. THe group name is set using the following format: `<organization>_<team name>` and contains underscores `_` instead of spaces.
A User who is added to an team as a "Manager" can modify the team members or if they have permission `access.change_team` which also allows the changing of team permissions. Modification of an organization can be done by the django administrator (super user) or any user with permission `access._change_organization`.
Items can be set as `Global`, meaning that all users who have the correct permission regardless of organization will be able to take action against the object.
Permissions that can be modified for a team have been limited to application permissions only unless adjust the permissions from the django admin site.
### Multi-Tenancy workflow
The workflow is conducted as part of the view and has the following flow:
1. Checks if user is member of organization the object the action is being performed on. Will also return true if the object has field `is_global` set to `true`.
1. Fetches all teams the user is part of.
1. obtains all permissions that are linked to the team.
1. checks if user has the required permission for the action.
1. confirms that the team the permission came from is part of the same organization as the object the action is being conducted on.
1. ONLY on success of the above items, grants access.
"""
permission_required: list = []
""" Permission required for the view
Not specifying this property adjusts the permission check logic so that you can
use the `permission_check()` function directly.
An example of a get request....
``` py
def get(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if not self.permission_check(request, [ 'access.view_organization' ]):
raise PermissionDenied('You are not part of this organization')
return super().get(request, *args, **kwargs)
```
this example details manual usage of the `permission_check()` function for a get request.
"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if len(self.permission_required) > 0:
if not self.permission_check(request):
raise PermissionDenied('You are not part of this organization')
return super().dispatch(self.request, *args, **kwargs)

View File

@ -37,6 +37,20 @@ class Organization(SaveHistory):
unique = True,
)
manager = models.ForeignKey(
User,
on_delete=models.SET_NULL,
blank = False,
null = True,
help_text = 'Organization Manager'
)
model_notes = models.TextField(
blank = True,
default = None,
null= True,
)
slug = AutoSlugField()
created = AutoCreatedField()
@ -73,6 +87,12 @@ class TenancyObject(models.Model):
blank = False
)
model_notes = models.TextField(
blank = True,
default = None,
null= True,
)
def get_organization(self) -> Organization:
return self.organization
@ -86,11 +106,12 @@ class Team(Group, TenancyObject, SaveHistory):
def __str__(self):
return self.name
def save(self, *args, **kwargs):
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
self.name = self.organization.name.lower().replace(' ', '_') + '_' + self.team_name.lower().replace(' ', '_')
super().save(*args, **kwargs)
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
team_name = models.CharField(
@ -106,6 +127,13 @@ class Team(Group, TenancyObject, SaveHistory):
modified = AutoLastModifiedField()
@property
def parent_object(self):
""" Fetch the parent object """
return self.organization
def permission_list(self) -> list:
permission_list = []
@ -190,3 +218,10 @@ class TeamUsers(SaveHistory):
user.groups.add(group)
@property
def parent_object(self):
""" Fetch the parent object """
return self.team

View File

@ -3,7 +3,7 @@
{% block title %}Organizations{% endblock %}
{% block content_header_icon %}{% endblock %}
{% block body%}
{% block content %}
<table class="data">
<tr>

View File

@ -1,19 +1,89 @@
{% extends 'base.html.j2' %}
{% load markdown %}
{% block title %}Organization - {{ organization.name }}{% endblock %}
{% block body%}
{% block content %}
<style>
form div .helptext {
background-color: rgb(0, 140, 255);
display: block;
}
<section class="content-header">
<fieldset><label>Name</label><!-- <input type="text" value="{{ organization.name }}" /> -->{{form.name}}</fieldset>
<fieldset><label>Created</label><input type="text" value="{{ organization.created }}" readonly /></fieldset>
<fieldset><label>Modified</label><input type="text" value="{{ organization.modified }}" readonly /></fieldset>
</section>
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
<input type="button" value="<< Back" onclick="window.location='{% url 'Access:Organizations' %}';">
<input type="button" value="New Team" onclick="window.location='{% url 'Access:_team_add' organization.id %}';">
.detail-view-field {
display:unset;
height: 30px;
line-height: 30px;
padding: 0px 20px 40px 20px;
}
.detail-view-field label {
display: inline-block;
font-weight: bold;
width: 200px;
margin: 10px;
/*padding: 10px;*/
height: 30px;
line-height: 30px;
}
.detail-view-field span {
display: inline-block;
width: 340px;
margin: 10px;
/*padding: 10px;*/
border-bottom: 1px solid #ccc;
height: 30px;
line-height: 30px;
}
</style>
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">
<div style="display: inline; width: 40%; margin: 30px;">
<div class="detail-view-field">
<label>{{ form.name.label }}</label>
<span>{{ form.name.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.manager.label }}</label>
<span>{{ form.manager.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.created.label }}</label>
<span>{{ form.created.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.modified.label }}</label>
<span>{{ form.modified.value }}</span>
</div>
</div>
<div style="display: inline; width: 40%; margin: 30px; text-align: left;">
<div>
<label style="font-weight: bold; width: 100%; border-bottom: 1px solid #ccc; display: block; text-align: inherit;">{{ form.model_notes.label }}</label>
<div style="display: inline-block; text-align: left;">{{ form.model_notes.value | markdown | safe }}</div>
</div>
</div>
<div style="display: block;">
<input type="button" value="<< Back" onclick="window.location='{% url 'Access:Organizations' %}';">
<input type="button" value="New Team" onclick="window.location='{% url 'Access:_team_add' organization.id %}';">
</div>
<hr />
<table>

View File

@ -2,26 +2,12 @@
{% block title %}Team - {{ team.team_name }}{% endblock %}
{% block body%}
{% block content %}
<form method="post">
{% csrf_token %}
<div>
<input name="organization" id="id_organization" type="hidden" value="{{ organization.id }}">
<section class="content-header">
<fieldset><label>Name</label><input name="name" required id="id_name" type="text" value="{{ team.team_name }}" /></fieldset>
<fieldset><label>Created</label><input name="created" type="text" value="{{ team.created }}" readonly /></fieldset>
<fieldset><label>Modified</label><input name="modified" type="text" value="{{ team.modified }}" readonly /></fieldset>
<fieldset><label>Permissions</label>
<select name="permissions" id="id_permissions" style="height: 200px;" multiple>
{% for permission in permissions %}
{% if 'administration' not in permission.content_type|lower and 'authorization' not in permission.content_type|lower and 'content types' not in permission.content_type|lower and 'session' not in permission.content_type|lower and 'python social auth' not in permission.content_type|lower and 'add_organization' not in permission.codename|lower and 'delete_organization' not in permission.codename|lower %}
<option value="{{ permission.id }}" {% for team_permission in team.permissions.all %}{% if permission.id == team_permission.id %}selected{% endif %}{% endfor %}>{{ permission.content_type }} | {{ permission.name }}</option>
{% endif %}
{% endfor %}
</select>
</fieldset>
</section>
</div>
{{ form.as_div }}
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
<input style="display:unset;" type="submit" value="Submit">
</form>

View File

View File

View File

@ -0,0 +1,251 @@
import pytest
import unittest
from django.test import Client
from django.shortcuts import reverse
class OrganizationManagerModelPermissionView:
""" Tests for checking Organization Manager model permissions """
app_namespace: str = None
""" Application namespace of the model being tested """
different_organization_is_manager: object
""" User whom is organization Manager of different organization than object """
url_name_view: str
""" url name of the model view to be tested """
url_view_kwargs: dict = None
""" View URL kwargs for model being tested """
user_is_organization_manager: object
""" User whom is organization Manager of the object"""
def test_model_view_different_organizaiton_is_organization_manager_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization whom is an organization Manager.
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_view, kwargs=self.url_view_kwargs)
else:
url = reverse(self.url_name_view, kwargs=self.url_view_kwargs)
client.force_login(self.different_organization_is_manager)
response = client.get(url)
assert response.status_code == 403
def test_model_view_has_no_permission_is_organization_manager(self):
""" Confirm that an organization manager can view the model
Attempt to view as user who is an organization manager and has no permissions assigned.
Object to be within same organization the user is a manager of.
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_view, kwargs=self.url_view_kwargs)
else:
url = reverse(self.url_name_view, kwargs=self.url_view_kwargs)
client.force_login(self.user_is_organization_manager)
response = client.get(url)
assert response.status_code == 200
class OrganizationManagerModelPermissionAdd:
""" Tests for checking model Add permissions """
app_namespace: str = None
""" Application namespace of the model being tested """
different_organization_is_manager: object
""" User whom is organization Manager of different organization than object """
url_name_view: str
""" url name of the model view to be tested """
url_view_kwargs: dict = None
""" View URL kwargs for model being tested """
user_is_organization_manager: object
""" User whom is organization Manager of the object"""
def test_model_add_different_organization_is_organization_manager_denied(self):
""" Check correct permission for add
attempt to add as user from different organization whom is an organization Manager.
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
client.force_login(self.different_organization_is_manager)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
def test_model_add_has_no_permission_is_organization_manager(self):
""" Check correct permission for add
Attempt to add as user who is an organization manager and has no permissions assigned.
Object to be within same organization the user is a manager of.
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
client.force_login(self.user_is_organization_manager)
response = client.post(url, data=self.add_data)
assert response.status_code == 200
class OrganizationManagerModelPermissionChange:
""" Tests for checking model change permissions """
app_namespace: str = None
""" Application namespace of the model being tested """
different_organization_is_manager: object
""" User whom is organization Manager of different organization than object """
url_name_change: str
""" url name of the model view to be tested """
url_change_kwargs: dict = None
""" View URL kwargs for model being tested """
user_is_organization_manager: object
""" User whom is organization Manager of the object"""
def test_model_change_different_organization_is_organization_manager_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization whom is an organization Manager.
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
client.force_login(self.different_organization_is_manager)
response = client.post(url, data=self.change_data)
assert response.status_code == 403
def test_model_change_has_no_permission_is_organization_manager(self):
""" Check correct permission for change
Make change as user who is an organization manager and has no permissions assigned.
Object to be within same organization the user is a manager of.
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
client.force_login(self.user_is_organization_manager)
response = client.post(url, data=self.change_data)
assert response.status_code == 200
class OrganizationManagerModelPermissionDelete:
""" Tests for checking model delete permissions """
app_namespace: str = None
""" Application namespace of the model being tested """
different_organization_is_manager: object
""" User whom is organization Manager of different organization than object """
url_name_view: str
""" url name of the model view to be tested """
url_view_kwargs: dict = None
""" View URL kwargs for model being tested """
user_is_organization_manager: object
""" User whom is organization Manager of the object"""
def test_model_delete_different_organization_is_organization_manager_denied(self):
""" Check correct permission for delete
Attempt to delete as user from different organization whom is an organization Manager.
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
client.force_login(self.different_organization_is_manager)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_model_delete_has_no_permission_is_organization_manager(self):
""" Check correct permission for delete
Delete item as user who is an organization manager and has no permissions assigned.
Object to be within same organization the user is a manager of.
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
client.force_login(self.user_is_organization_manager)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 302 and response.url == self.url_delete_response
class OrganizationManagerModelPermissions(
OrganizationManagerModelPermissionView,
OrganizationManagerModelPermissionAdd,
OrganizationManagerModelPermissionChange,
OrganizationManagerModelPermissionDelete
):
""" Tests for checking Organization Manager model permissions
This class includes all test cases for: Add, Change, Delete and View.
"""
app_namespace: str = None

View File

@ -10,14 +10,32 @@ import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from access.tests.abstract.model_permissions_organization_manager import OrganizationManagerModelPermissionChange, OrganizationManagerModelPermissionView
from app.tests.abstract.model_permissions import ModelPermissionsView, ModelPermissionsChange
class OrganizationPermissions(TestCase):
class OrganizationPermissions(
TestCase,
ModelPermissionsView,
ModelPermissionsChange,
OrganizationManagerModelPermissionChange,
OrganizationManagerModelPermissionView,
):
model = Organization
model_name = 'organization'
app_label = 'access'
app_namespace = 'Access'
url_name_view = '_organization_view'
# url_name_add = '_organization_add'
url_name_change = '_organization_view'
# url_name_delete = '_organization_delete'
# url_delete_response = reverse('ITAM:Operating Systems')
@classmethod
def setUpTestData(self):
@ -34,7 +52,11 @@ class OrganizationPermissions(TestCase):
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
different_organization = Organization.objects.create(
name='test_different_organization'
)
self.different_organization = different_organization
# self.item = self.model.objects.create(
@ -44,11 +66,27 @@ class OrganizationPermissions(TestCase):
self.item = organization
self.url_view_kwargs = {'pk': self.item.id}
# self.url_add_kwargs = {'pk': self.item.id}
# self.add_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
self.url_change_kwargs = {'pk': self.item.id}
self.change_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
# self.url_delete_kwargs = {'pk': self.item.id}
# self.delete_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
view_permissions = Permission.objects.get(
codename = 'view_' + self.model_name,
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -62,10 +100,10 @@ class OrganizationPermissions(TestCase):
add_permissions = Permission.objects.get(
codename = 'add_' + self.model_name,
codename = 'add_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -79,10 +117,10 @@ class OrganizationPermissions(TestCase):
change_permissions = Permission.objects.get(
codename = 'change_' + self.model_name,
codename = 'change_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -96,10 +134,10 @@ class OrganizationPermissions(TestCase):
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model_name,
codename = 'delete_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -159,374 +197,18 @@ class OrganizationPermissions(TestCase):
user = self.different_organization_user
)
def test_organization_auth_view_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
response = client.get(url)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_organization_auth_view_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
def test_organization_auth_view_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
def test_organization_auth_view_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200
@pytest.mark.skip(reason="No Add view exists")
def test_organization_auth_add_user_anon_denied(self):
""" Check correct permission for add
Attempt to add as anon user
"""
client = Client()
url = reverse('Access:_organization_add')
response = client.put(url, data={'device': 'device'})
assert (
response.status_code == 302
or
response.status_code == 403
self.user_is_organization_manager = User.objects.create_user(
username="test_org_manager",
password="password"
)
self.organization.manager = self.user_is_organization_manager
self.organization.save()
@pytest.mark.skip(reason="No Add view exists")
def test_organization_auth_add_no_permission_denied(self):
""" Check correct permission for add
Attempt to add as user with no permissions
"""
client = Client()
url = reverse('Access:_organization_add')
client.force_login(self.no_permissions_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="No Add view exists")
def test_organization_auth_add_different_organization_denied(self):
""" Check correct permission for add
attempt to add as user from different organization
"""
client = Client()
url = reverse('Access:_organization_add')
client.force_login(self.different_organization_user)
response = client.post(url, data={'name': 'device', 'organization': self.organization.id})
assert response.status_code == 403
@pytest.mark.skip(reason="No Add view exists")
def test_organization_auth_add_permission_view_denied(self):
""" Check correct permission for add
Attempt to add a user with view permission
"""
client = Client()
url = reverse('Access:_organization_add')
client.force_login(self.view_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="No Add view exists")
def test_organization_auth_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with no permission
"""
client = Client()
url = reverse('Access:_organization_add')
client.force_login(self.add_user)
response = client.post(url, data={'device': 'device', 'organization': self.organization.id})
assert response.status_code == 200
def test_organization_auth_change_user_anon_denied(self):
""" Check correct permission for change
Attempt to change as anon
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
response = client.patch(url, data={'device': 'device'})
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_organization_auth_change_no_permission_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user without permissions
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_organization_auth_change_different_organization_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_organization_auth_change_permission_view_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with view permission
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_organization_auth_change_permission_add_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with add permission
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
client.force_login(self.add_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_organization_auth_change_has_permission(self):
""" Check correct permission for change
Make change with user who has change permission
"""
client = Client()
url = reverse('Access:_organization_view', kwargs={'pk': self.item.id})
client.force_login(self.change_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 200
@pytest.mark.skip(reason="No Delete view exists")
def test_organization_auth_delete_user_anon_denied(self):
""" Check correct permission for delete
Attempt to delete item as anon user
"""
client = Client()
url = reverse('Access:_organization_delete', kwargs={'pk': self.item.id})
response = client.delete(url, data={'device': 'device'})
assert (
response.status_code == 302
or
response.status_code == 403
self.different_organization_is_manager = User.objects.create_user(
username="test_org_manager_different_org",
password="password"
)
@pytest.mark.skip(reason="No Delete view exists")
def test_organization_auth_delete_no_permission_denied(self):
""" Check correct permission for delete
Attempt to delete as user with no permissons
"""
client = Client()
url = reverse('Access:_organization_delete', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="No Delete view exists")
def test_organization_auth_delete_different_organization_denied(self):
""" Check correct permission for delete
Attempt to delete as user from different organization
"""
client = Client()
url = reverse('Access:_organization_delete', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="No Delete view exists")
def test_organization_auth_delete_permission_view_denied(self):
""" Check correct permission for delete
Attempt to delete as user with veiw permission only
"""
client = Client()
url = reverse('Access:_organization_delete', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="No Delete view exists")
def test_organization_auth_delete_permission_add_denied(self):
""" Check correct permission for delete
Attempt to delete as user with add permission only
"""
client = Client()
url = reverse('Access:_organization_delete', kwargs={'pk': self.item.id})
client.force_login(self.add_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="No Delete view exists")
def test_organization_auth_delete_permission_change_denied(self):
""" Check correct permission for delete
Attempt to delete as user with change permission only
"""
client = Client()
url = reverse('Access:_organization_delete', kwargs={'pk': self.item.id})
client.force_login(self.change_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
@pytest.mark.skip(reason="No Delete view exists")
def test_organization_auth_delete_has_permission(self):
""" Check correct permission for delete
Delete item as user with delete permission
"""
client = Client()
url = reverse('Access:_organization_delete', kwargs={'pk': self.item.id})
client.force_login(self.delete_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 302 and response.url == reverse('Access:Devices')
self.different_organization.manager = self.different_organization_is_manager
self.different_organization.save()

View File

@ -1,23 +1,35 @@
import pytest
import unittest
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 rest_framework.test import APIClient as Client
import pytest
import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
class OrganizationPermissionsAPI(TestCase):
from api.tests.abstract.api_permissions import APIPermissionChange, APIPermissionView
class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionView):
model = Organization
model_name = 'organization'
app_label = 'access'
app_namespace = 'API'
url_name = '_api_organization'
url_list = 'device-list'
change_data = {'name': 'device'}
# delete_data = {'device': 'device'}
@classmethod
def setUpTestData(self):
"""Setup Test
@ -38,11 +50,18 @@ class OrganizationPermissionsAPI(TestCase):
self.item = organization
self.url_view_kwargs = {'pk': self.item.id}
self.url_kwargs = {'pk': self.item.id}
# self.add_data = {'name': 'device', 'organization': self.organization.id}
view_permissions = Permission.objects.get(
codename = 'view_' + self.model_name,
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -56,10 +75,10 @@ class OrganizationPermissionsAPI(TestCase):
add_permissions = Permission.objects.get(
codename = 'add_' + self.model_name,
codename = 'add_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -73,10 +92,10 @@ class OrganizationPermissionsAPI(TestCase):
change_permissions = Permission.objects.get(
codename = 'change_' + self.model_name,
codename = 'change_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -90,10 +109,10 @@ class OrganizationPermissionsAPI(TestCase):
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model_name,
codename = 'delete_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -152,375 +171,3 @@ class OrganizationPermissionsAPI(TestCase):
team = different_organization_team,
user = self.different_organization_user
)
def test_organization_auth_view_user_anon_denied_api(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse('API:_api_organization', kwargs={'pk': self.item.id})
response = client.get(url)
assert response.status_code == 401
def test_organization_auth_view_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse('API:_api_organization', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
def test_organization_auth_view_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse('API:_api_organization', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
def test_organization_auth_view_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse('API:_api_organization', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200
# @pytest.mark.skip(reason="currently only able to add via admin interface")
# def test_organization_auth_add_user_anon_denied(self):
# """ Check correct permission for add
# Attempt to add as anon user
# """
# client = Client()
# url = reverse('API:_api_orgs')
# response = client.post(url, data={'device': 'device'})
# assert (
# response.status_code == 302
# or
# response.status_code == 403
# )
# @pytest.mark.skip(reason="currently only able to add via admin interface")
# def test_organization_auth_add_no_permission_denied(self):
# """ Check correct permission for add
# Attempt to add as user with no permissions
# """
# client = Client()
# url = reverse('API:_api_orgs')
# client.force_login(self.no_permissions_user)
# response = client.post(url, data={'device': 'device'})
# assert response.status_code == 403
# @pytest.mark.skip(reason="currently only able to add via admin interface")
# def test_organization_auth_add_different_organization_denied(self):
# """ Check correct permission for add
# attempt to add as user from different organization
# """
# client = Client()
# url = reverse('API:_api_orgs')
# client.force_login(self.different_organization_user)
# response = client.post(url, data={'name': 'device', 'organization': self.organization.id})
# assert response.status_code == 403
# @pytest.mark.skip(reason="currently only able to add via admin interface")
# def test_organization_auth_add_permission_view_denied(self):
# """ Check correct permission for add
# Attempt to add a user with view permission
# """
# client = Client()
# url = reverse('API:_api_orgs')
# client.force_login(self.view_user)
# response = client.post(url, data={'device': 'device'})
# assert response.status_code == 403
# @pytest.mark.skip(reason="currently only able to add via admin interface")
# def test_organization_auth_add_has_permission(self):
# """ Check correct permission for add
# Attempt to add as user with no permission
# """
# client = Client()
# url = reverse('API:_api_orgs')
# client.force_login(self.add_user)
# response = client.post(url, data={'device': 'device', 'organization': self.organization.id})
# assert response.status_code == 200
def test_organization_auth_change_user_anon_denied(self):
""" Check correct permission for change
Attempt to change as anon
"""
client = Client()
url = reverse('API:_api_organization', kwargs={'pk': self.item.id})
response = client.patch(url, data={'device': 'device'})
assert response.status_code == 401
def test_organization_auth_change_no_permission_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user without permissions
"""
client = Client()
url = reverse('API:_api_organization', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.patch(url, data={'device': 'device'})
assert response.status_code == 403
def test_organization_auth_change_different_organization_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization
"""
client = Client()
url = reverse('API:_api_organization', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.patch(url, data={'device': 'device'})
assert response.status_code == 403
def test_organization_auth_change_permission_view_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with view permission
"""
client = Client()
url = reverse('API:_api_organization', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.patch(url, data={'device': 'device'})
assert response.status_code == 403
def test_organization_auth_change_permission_add_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with add permission
"""
client = Client()
url = reverse('API:_api_organization', kwargs={'pk': self.item.id})
client.force_login(self.add_user)
response = client.patch(url, data={'device': 'device'})
assert response.status_code == 403
def test_organization_auth_change_has_permission(self):
""" Check correct permission for change
Make change with user who has change permission
"""
client = Client()
url = reverse('API:_api_organization', kwargs={'pk': self.item.id})
client.force_login(self.change_user)
response = client.patch(url, data={'device': 'device'})
assert response.status_code == 200
# @pytest.mark.skip(reason="currently only able to add via admin interface")
# def test_organization_auth_delete_user_anon_denied(self):
# """ Check correct permission for delete
# Attempt to delete item as anon user
# """
# client = Client()
# url = reverse('API:_api_orgs', kwargs={'pk': self.item.id})
# response = client.delete(url, data={'device': 'device'})
# assert (
# response.status_code == 302
# or
# response.status_code == 403
# )
# @pytest.mark.skip(reason="currently only able to add via admin interface")
# def test_organization_auth_delete_no_permission_denied(self):
# """ Check correct permission for delete
# Attempt to delete as user with no permissons
# """
# client = Client()
# url = reverse('API:_api_organization', kwargs={'pk': self.item.id})
# client.force_login(self.no_permissions_user)
# response = client.delete(url, data={'device': 'device'})
# assert response.status_code == 403
# @pytest.mark.skip(reason="currently only able to add via admin interface")
# def test_organization_auth_delete_different_organization_denied(self):
# """ Check correct permission for delete
# Attempt to delete as user from different organization
# """
# client = Client()
# url = reverse('API:_api_organization', kwargs={'pk': self.item.id})
# client.force_login(self.different_organization_user)
# response = client.delete(url, data={'device': 'device'})
# assert response.status_code == 403
# @pytest.mark.skip(reason="currently only able to add via admin interface")
# def test_organization_auth_delete_permission_view_denied(self):
# """ Check correct permission for delete
# Attempt to delete as user with veiw permission only
# """
# client = Client()
# url = reverse('API:_api_organization', kwargs={'pk': self.item.id})
# client.force_login(self.view_user)
# response = client.delete(url, data={'device': 'device'})
# assert response.status_code == 403
# @pytest.mark.skip(reason="currently only able to add via admin interface")
# def test_organization_auth_delete_permission_add_denied(self):
# """ Check correct permission for delete
# Attempt to delete as user with add permission only
# """
# client = Client()
# url = reverse('API:_api_organization', kwargs={'pk': self.item.id})
# client.force_login(self.add_user)
# response = client.delete(url, data={'device': 'device'})
# assert response.status_code == 403
# @pytest.mark.skip(reason="currently only able to add via admin interface")
# def test_organization_auth_delete_permission_change_denied(self):
# """ Check correct permission for delete
# Attempt to delete as user with change permission only
# """
# client = Client()
# url = reverse('API:_api_organization', kwargs={'pk': self.item.id})
# client.force_login(self.change_user)
# response = client.delete(url, data={'device': 'device'})
# assert response.status_code == 403
# @pytest.mark.skip(reason="currently only able to add via admin interface")
# def test_organization_auth_delete_has_permission(self):
# """ Check correct permission for delete
# Delete item as user with delete permission
# """
# client = Client()
# url = reverse('API:_api_organization', kwargs={'pk': self.item.id})
# client.force_login(self.delete_user)
# response = client.delete(url, data={'device': 'device'})
# assert response.status_code == 302 and response.url == reverse('API:_api_orgs')

View File

@ -12,87 +12,6 @@ from core.models.history import History
from access.models import Organization
# @pytest.mark.skip(reason="to be written")
# def test_history_auth_view():
# """ User requires Permission view_history """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_create():
# """ History row must be added to history table on create """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_update():
# """ History row must be added to history table on updatej """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_delete():
# """ History row must be added to history table on delete """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
class OrganizationHistory(TestCase):
@ -130,8 +49,24 @@ class OrganizationHistory(TestCase):
item_class = self.model._meta.model_name,
)
self.item_delete = self.model.objects.create(
name = 'test_item_delete_' + self.model_name,
)
self.item_delete.delete()
self.history_delete = History.objects.filter(
item_pk = self.item_delete.pk,
item_class = self.model._meta.model_name,
)
self.history_delete_children = History.objects.filter(
item_parent_pk = self.item_delete.pk,
item_parent_class = self.model._meta.model_name,
)
# field type testing to be done as part of model testing
def test_history_entry_item_add_field_action(self):
""" Ensure action is "add" for item creation """
@ -185,7 +120,6 @@ class OrganizationHistory(TestCase):
# field type testing to be done as part of model testing
def test_history_entry_item_change_field_action(self):
""" Ensure action is "add" for item creation """
@ -232,3 +166,22 @@ class OrganizationHistory(TestCase):
# assert type(history['item_class']) is str
################################## Delete ##################################
def test_device_history_entry_delete(self):
""" When an item is deleted, it's history entries must be removed """
assert self.history_delete.exists() is False
def test_device_history_entry_children_delete(self):
""" When an item is deleted, it's history entries must be removed """
assert self.history_delete_children.exists() is False

View File

@ -14,7 +14,7 @@ from access.models import Organization, Team, TeamUsers, Permission
from core.models.history import History
class DeviceHistoryPermissions(TestCase):
class OrganizationHistoryPermissions(TestCase):
item_model = Organization

View File

@ -0,0 +1,56 @@
import pytest
import unittest
from django.test import TestCase, Client
from access.models import Organization, Team, TeamUsers, Permission
class TeamModel(TestCase):
model = Team
@classmethod
def setUpTestData(self):
""" Setup Test
"""
self.parent_item = Organization.objects.create(name='test_org')
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.model.objects.create(
organization=self.parent_item,
name = 'teamone'
)
def test_model_has_property_parent_object(self):
""" Check if model contains 'parent_object'
This is a required property for all models that have a parent
"""
assert hasattr(self.model, 'parent_object')
def test_model_property_parent_object_returns_object(self):
""" Check if model contains 'parent_object'
This is a required property for all models that have a parent
"""
assert self.item.parent_object is self.parent_item
@pytest.mark.skip(reason="to be written")
def test_function_save_attributes():
""" Ensure save Attributes function match django default
the save method is overridden. the function attributes must match default django method
"""
pass

View File

@ -8,98 +8,19 @@ from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from core.tests.abstract.history_entry import HistoryEntry
from core.tests.abstract.history_entry_child_model import HistoryEntryChildItem
from access.models import Team
# @pytest.mark.skip(reason="to be written")
# def test_history_auth_view():
# """ User requires Permission view_history """
# pass
from django.contrib.auth.models import Group
# @pytest.mark.skip(reason="to be written")
# def test_history_device_create():
# """ History row must be added to history table on create """
# pass
class TeamHistory(TestCase, HistoryEntry, HistoryEntryChildItem):
# @pytest.mark.skip(reason="to be written")
# def test_history_device_update():
# """ History row must be added to history table on updatej """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_delete():
# """ History row must be added to history table on delete """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
class TeamHistory(TestCase):
model = Team
model_name = 'team'
@classmethod
def setUpTestData(self):
@ -109,8 +30,10 @@ class TeamHistory(TestCase):
self.organization = organization
self.item_parent = organization
self.item_create = self.model.objects.create(
name = 'test_item_' + self.model_name,
team_name = 'test_item_' + self.model._meta.model_name,
organization = self.organization
)
@ -122,122 +45,35 @@ class TeamHistory(TestCase):
)
self.item_change = self.item_create
self.item_change.name = 'test_item_' + self.model_name + '_changed'
self.item_change.team_name = 'test_item_' + self.model._meta.model_name + '_changed'
self.item_change.save()
self.field_after_expected_value = '{"name": "test_org_' + self.item_change.team_name + '", "team_name": "' + self.item_change.team_name + '"}'
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
debug = Group.objects.all()
@pytest.mark.skip(reason="fails, fixme see #46")
# field type testing to be done as part of model testing
def test_history_entry_item_add_field_action(self):
""" Ensure action is "add" for item creation """
self.item_delete = self.model.objects.create(
team_name = 'test_item_delete_' + self.model._meta.model_name,
organization = self.organization
)
history = self.history_create.__dict__
self.deleted_pk = self.item_delete.pk
assert history['action'] == int(History.Actions.ADD[0])
# assert type(history['action']) is int
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_add_field_after(self):
""" Ensure after field contains correct value """
history = self.history_create.__dict__
assert history['after'] == str('{}')
# assert type(history['after']) is str
@pytest.mark.skip(reason="fails, fixme see #46")
def test_history_entry_item_add_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_create.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
@pytest.mark.skip(reason="fails, fixme see #46")
def test_history_entry_item_add_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_create.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
@pytest.mark.skip(reason="fails, fixme see #46")
def test_history_entry_item_add_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_create.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
################################## Change ##################################
@pytest.mark.skip(reason="fails, fixme see #46")
# field type testing to be done as part of model testing
def test_history_entry_item_change_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_change.__dict__
assert history['action'] == int(History.Actions.UPDATE[0])
# assert type(history['action']) is int
@pytest.mark.skip(reason="fails, fixme see #46")
def test_history_entry_item_change_field_after(self):
""" Ensure after field contains correct value """
history = self.history_change.__dict__
assert history['after'] == str('{"name": "test_item_' + self.model_name + '_changed"}')
# assert type(history['after']) is str
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_change_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_change.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
@pytest.mark.skip(reason="fails, fixme see #46")
def test_history_entry_item_change_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_change.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
@pytest.mark.skip(reason="fails, fixme see #46")
def test_history_entry_item_change_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_change.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
self.item_delete.delete()
self.history_delete = History.objects.get(
action = History.Actions.DELETE[0],
item_pk = self.deleted_pk,
item_class = self.model._meta.model_name,
)
self.history_delete_children = History.objects.filter(
item_parent_pk = self.deleted_pk,
item_parent_class = self.item_parent._meta.model_name,
)

View File

@ -10,15 +10,30 @@ import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from access.tests.abstract.model_permissions_organization_manager import OrganizationManagerModelPermissions
from app.tests.abstract.model_permissions import ModelPermissions
class TeamPermissions(TestCase):
class TeamPermissions(
TestCase,
ModelPermissions,
OrganizationManagerModelPermissions,
):
model = Team
model_name = 'team'
app_label = 'access'
app_namespace = 'Access'
url_name_view = '_team_view'
url_name_add = '_team_add'
url_name_change = '_team_view'
url_name_delete = '_team_delete'
@classmethod
def setUpTestData(self):
@ -37,17 +52,37 @@ class TeamPermissions(TestCase):
different_organization = Organization.objects.create(name='test_different_organization')
self.different_organization = different_organization
self.item = self.model.objects.create(
organization=organization,
name = 'teamone'
)
self.url_view_kwargs = {'organization_id': self.organization.id, 'pk': self.item.id}
self.url_add_kwargs = {'pk': self.organization.id}
self.add_data = {'team': 'team'}
self.url_change_kwargs = {'organization_id': self.organization.id, 'pk': self.item.id}
self.change_data = {'team': 'team'}
self.url_delete_kwargs = {'organization_id': self.organization.id, 'pk': self.item.id}
self.delete_data = {'team': 'team'}
self.url_delete_response = reverse('Access:_organization_view', kwargs={'pk': self.organization.id})
view_permissions = Permission.objects.get(
codename = 'view_' + self.model_name,
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -61,10 +96,10 @@ class TeamPermissions(TestCase):
add_permissions = Permission.objects.get(
codename = 'add_' + self.model_name,
codename = 'add_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -78,10 +113,10 @@ class TeamPermissions(TestCase):
change_permissions = Permission.objects.get(
codename = 'change_' + self.model_name,
codename = 'change_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -95,10 +130,10 @@ class TeamPermissions(TestCase):
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model_name,
codename = 'delete_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -158,353 +193,18 @@ class TeamPermissions(TestCase):
user = self.different_organization_user
)
self.user_is_organization_manager = User.objects.create_user(
username="test_org_manager",
password="password"
)
self.organization.manager = self.user_is_organization_manager
self.organization.save()
def test_team_auth_view_user_anon_denied(self):
""" Check correct permission for view
self.different_organization_is_manager = User.objects.create_user(
username="test_org_manager_different_org",
password="password"
)
Attempt to view as anon user
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
response = client.get(url)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_team_auth_view_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
def test_team_auth_view_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
def test_team_auth_view_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200
def test_team_auth_add_user_anon_denied(self):
""" Check correct permission for add
Attempt to add as anon user
"""
client = Client()
url = reverse('Access:_team_add', kwargs={'pk': self.organization.id})
response = client.put(url, data={'team': 'team'})
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_team_auth_add_no_permission_denied(self):
""" Check correct permission for add
Attempt to add as user with no permissions
"""
client = Client()
url = reverse('Access:_team_add', kwargs={'pk': self.organization.id})
client.force_login(self.no_permissions_user)
response = client.post(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_add_different_organization_denied(self):
""" Check correct permission for add
attempt to add as user from different organization
"""
client = Client()
url = reverse('Access:_team_add', kwargs={'pk': self.organization.id})
client.force_login(self.different_organization_user)
response = client.post(url, data={'name': 'team', 'organization': self.organization.id})
assert response.status_code == 403
def test_team_auth_add_permission_view_denied(self):
""" Check correct permission for add
Attempt to add a user with view permission
"""
client = Client()
url = reverse('Access:_team_add', kwargs={'pk': self.organization.id})
client.force_login(self.view_user)
response = client.post(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with no permission
"""
client = Client()
url = reverse('Access:_team_add', kwargs={'pk': self.organization.id})
client.force_login(self.add_user)
response = client.post(url, data={'team': 'team', 'organization': self.organization.id})
assert response.status_code == 200
def test_team_auth_change_user_anon_denied(self):
""" Check correct permission for change
Attempt to change as anon
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
response = client.patch(url, data={'team': 'team'})
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_team_auth_change_no_permission_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user without permissions
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.post(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_change_different_organization_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.post(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_change_permission_view_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with view permission
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.view_user)
response = client.post(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_change_permission_add_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with add permission
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.add_user)
response = client.post(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_change_has_permission(self):
""" Check correct permission for change
Make change with user who has change permission
"""
client = Client()
url = reverse('Access:_team_view', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.change_user)
response = client.post(url, data={'team': 'team'})
assert response.status_code == 200
def test_team_auth_delete_user_anon_denied(self):
""" Check correct permission for delete
Attempt to delete item as anon user
"""
client = Client()
url = reverse('Access:_team_delete', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_team_auth_delete_no_permission_denied(self):
""" Check correct permission for delete
Attempt to delete as user with no permissons
"""
client = Client()
url = reverse('Access:_team_delete', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_delete_different_organization_denied(self):
""" Check correct permission for delete
Attempt to delete as user from different organization
"""
client = Client()
url = reverse('Access:_team_delete', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_delete_permission_view_denied(self):
""" Check correct permission for delete
Attempt to delete as user with veiw permission only
"""
client = Client()
url = reverse('Access:_team_delete', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.view_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_delete_permission_add_denied(self):
""" Check correct permission for delete
Attempt to delete as user with add permission only
"""
client = Client()
url = reverse('Access:_team_delete', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.add_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_delete_permission_change_denied(self):
""" Check correct permission for delete
Attempt to delete as user with change permission only
"""
client = Client()
url = reverse('Access:_team_delete', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.change_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_delete_has_permission(self):
""" Check correct permission for delete
Delete item as user with delete permission
"""
client = Client()
url = reverse('Access:_team_delete', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.delete_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 302 and response.url == reverse('Access:_organization_view', kwargs={'pk': self.organization.id})
self.different_organization.manager = self.different_organization_is_manager
self.different_organization.save()

View File

@ -1,26 +1,32 @@
# from django.conf import settings
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 as nClient
from rest_framework.test import APIClient as Client
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.test import TestCase
from access.models import Organization, Team, TeamUsers, Permission
from api.tests.abstract.api_permissions import APIPermissions
class TeamPermissionsAPI(TestCase):
class TeamPermissionsAPI(TestCase, APIPermissions):
model = Team
model_name = 'team'
app_label = 'access'
app_namespace = 'API'
url_name = '_api_team'
url_list = '_api_organization_teams'
change_data = {'name': 'device'}
delete_data = {'device': 'device'}
@classmethod
def setUpTestData(self):
@ -45,11 +51,19 @@ class TeamPermissionsAPI(TestCase):
name = 'teamone'
)
self.url_kwargs = {'organization_id': self.organization.id}
self.url_view_kwargs = {'organization_id': self.organization.id, 'group_ptr_id': self.item.id}
self.add_data = {'team_name': 'team_post'}
view_permissions = Permission.objects.get(
codename = 'view_' + self.model_name,
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -63,10 +77,10 @@ class TeamPermissionsAPI(TestCase):
add_permissions = Permission.objects.get(
codename = 'add_' + self.model_name,
codename = 'add_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -80,10 +94,10 @@ class TeamPermissionsAPI(TestCase):
change_permissions = Permission.objects.get(
codename = 'change_' + self.model_name,
codename = 'change_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -97,10 +111,10 @@ class TeamPermissionsAPI(TestCase):
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model_name,
codename = 'delete_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -159,354 +173,3 @@ class TeamPermissionsAPI(TestCase):
team = different_organization_team,
user = self.different_organization_user
)
def test_team_auth_view_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
response = client.get(url)
assert response.status_code == 401
def test_team_auth_view_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
def test_team_auth_view_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
def test_team_auth_view_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200
def test_team_auth_add_user_anon_denied(self):
""" Check correct permission for add
Attempt to add as anon user
"""
client = Client()
url = reverse('API:_api_organization_teams', kwargs={'organization_id': self.organization.id})
response = client.post(url, data={'team_name': 'team'})
assert response.status_code == 401
def test_team_auth_add_no_permission_denied(self):
""" Check correct permission for add
Attempt to add as user with no permissions
"""
client = Client()
url = reverse('API:_api_organization_teams', kwargs={'organization_id': self.organization.id})
client.force_login(self.no_permissions_user)
response = client.post(url, data={'team_name': 'team'})
assert response.status_code == 403
def test_team_auth_add_different_organization_denied(self):
""" Check correct permission for add
attempt to add as user from different organization
"""
client = Client()
url = reverse('API:_api_organization_teams', kwargs={'organization_id': self.organization.id})
client.force_login(self.different_organization_user)
response = client.post(url, data={'team_name': 'team'})
assert response.status_code == 403
def test_team_auth_add_permission_view_denied(self):
""" Check correct permission for add
Attempt to add a user with view permission
"""
client = Client()
url = reverse('API:_api_organization_teams', kwargs={'organization_id': self.organization.id})
client.force_login(self.view_user)
response = client.post(url, data={'team_name': 'team'})
assert response.status_code == 403
def test_team_auth_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with no permission
"""
client = Client()
url = reverse('API:_api_organization_teams', kwargs={'organization_id': self.organization.id})
client.force_login(self.add_user)
response = client.post(url, {'team_name': 'team_post'})
assert response.status_code == 201
def test_team_auth_change_user_anon_denied(self):
""" Check correct permission for change
Attempt to change as anon
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
response = client.patch(url, data={'team': 'team'})
assert response.status_code == 401
def test_team_auth_change_no_permission_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user without permissions
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
client.force_login(self.no_permissions_user)
response = client.patch(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_change_different_organization_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
client.force_login(self.different_organization_user)
response = client.patch(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_change_permission_view_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with view permission
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
client.force_login(self.view_user)
response = client.patch(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_change_permission_add_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with add permission
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
client.force_login(self.add_user)
response = client.patch(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_change_has_permission(self):
""" Check correct permission for change
Make change with user who has change permission
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
client.force_login(self.change_user)
response = client.patch(url, data={'id': self.item.id, 'team_name': 'team'})
assert response.status_code == 200
def test_team_auth_delete_user_anon_denied(self):
""" Check correct permission for delete
Attempt to delete item as anon user
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 401
def test_team_auth_delete_no_permission_denied(self):
""" Check correct permission for delete
Attempt to delete as user with no permissons
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
client.force_login(self.no_permissions_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_delete_different_organization_denied(self):
""" Check correct permission for delete
Attempt to delete as user from different organization
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
client.force_login(self.different_organization_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_delete_permission_view_denied(self):
""" Check correct permission for delete
Attempt to delete as user with veiw permission only
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
client.force_login(self.view_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_delete_permission_add_denied(self):
""" Check correct permission for delete
Attempt to delete as user with add permission only
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
client.force_login(self.add_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_delete_permission_change_denied(self):
""" Check correct permission for delete
Attempt to delete as user with change permission only
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
client.force_login(self.change_user)
response = client.delete(url, data={'team': 'team'})
assert response.status_code == 403
def test_team_auth_delete_has_permission(self):
""" Check correct permission for delete
Delete item as user with delete permission
"""
client = Client()
url = reverse('API:_api_team', kwargs={'organization_id': self.organization.id, 'group_ptr_id': self.item.id})
client.force_login(self.delete_user)
response = client.delete(url, data={'group_ptr_id': self.item.id}, content_type='application/json')
assert response.status_code == 204

View File

@ -0,0 +1,56 @@
import pytest
import unittest
from django.test import TestCase, Client
from django.contrib.auth.models import User
from access.models import Organization, Team, TeamUsers, Permission
class TeamUsersModel(TestCase):
model = TeamUsers
@classmethod
def setUpTestData(self):
""" Setup Test
"""
organization = Organization.objects.create(name='test_org')
different_organization = Organization.objects.create(name='test_different_organization')
self.parent_item = Team.objects.create(
team_name = 'test_team',
organization = organization,
)
team_user = User.objects.create_user(username="test_self.team_user", password="password")
self.item = self.model.objects.create(
team = self.parent_item,
user = team_user
)
def test_model_has_property_parent_object(self):
""" Check if model contains 'parent_object'
This is a required property for all models that have a parent
"""
assert hasattr(self.model, 'parent_object')
def test_model_property_parent_object_returns_object(self):
""" Check if model contains 'parent_object'
This is a required property for all models that have a parent
"""
assert self.item.parent_object == self.parent_item

View File

@ -3,98 +3,20 @@ import pytest
import unittest
import requests
from django.contrib.auth.models import User
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from core.tests.abstract.history_entry import HistoryEntry
from core.tests.abstract.history_entry_child_model import HistoryEntryChildItem
from access.models import TeamUsers
# @pytest.mark.skip(reason="to be written")
# def test_history_auth_view():
# """ User requires Permission view_history """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_create():
# """ History row must be added to history table on create """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_update():
# """ History row must be added to history table on updatej """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_delete():
# """ History row must be added to history table on delete """
# pass
from access.models import Team, TeamUsers
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
@pytest.mark.skip(reason="to do")
class TeamUsersHistory(TestCase):
class TeamUsersHistory(TestCase, HistoryEntry, HistoryEntryChildItem):
model = TeamUsers
@ -109,11 +31,21 @@ class TeamUsersHistory(TestCase):
self.organization = organization
self.item_create = self.model.objects.create(
name = 'test_item_' + self.model_name,
self.item_parent = Team.objects.create(
team_name = 'test_item_' + self.model._meta.model_name,
organization = self.organization
)
self.user = User.objects.create(
username = 'test_item_' + self.model._meta.model_name,
password = 'a random password'
)
self.item_create = self.model.objects.create(
user = self.user,
team = self.item_parent
)
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
@ -122,9 +54,11 @@ class TeamUsersHistory(TestCase):
)
self.item_change = self.item_create
self.item_change.name = 'test_item_' + self.model_name + '_changed'
self.item_change.manager = True
self.item_change.save()
self.field_after_expected_value = '{"manager": true}'
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
@ -132,112 +66,27 @@ class TeamUsersHistory(TestCase):
)
@pytest.mark.skip(reason="to do")
# field type testing to be done as part of model testing
def test_history_entry_item_add_field_action(self):
""" Ensure action is "add" for item creation """
self.user_delete = User.objects.create(
username = 'test_item_delete' + self.model._meta.model_name,
password = 'a random password'
)
history = self.history_create.__dict__
self.item_delete = self.model.objects.create(
user = self.user_delete,
team = self.item_parent
)
assert history['action'] == int(History.Actions.ADD[0])
# assert type(history['action']) is int
self.deleted_pk = self.item_delete.pk
self.item_delete.delete()
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_add_field_after(self):
""" Ensure after field contains correct value """
history = self.history_create.__dict__
assert history['after'] == str('{}')
# assert type(history['after']) is str
@pytest.mark.skip(reason="to do")
def test_history_entry_item_add_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_create.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
@pytest.mark.skip(reason="to do")
def test_history_entry_item_add_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_create.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
@pytest.mark.skip(reason="to do")
def test_history_entry_item_add_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_create.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
################################## Change ##################################
@pytest.mark.skip(reason="to do")
# field type testing to be done as part of model testing
def test_history_entry_item_change_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_change.__dict__
assert history['action'] == int(History.Actions.UPDATE[0])
# assert type(history['action']) is int
@pytest.mark.skip(reason="to do")
def test_history_entry_item_change_field_after(self):
""" Ensure after field contains correct value """
history = self.history_change.__dict__
assert history['after'] == str('{"name": "test_item_' + self.model_name + '_changed"}')
# assert type(history['after']) is str
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_change_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_change.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
@pytest.mark.skip(reason="to do")
def test_history_entry_item_change_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_change.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
@pytest.mark.skip(reason="to do")
def test_history_entry_item_change_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_change.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
self.history_delete = History.objects.get(
action = History.Actions.DELETE[0],
item_pk = self.deleted_pk,
item_class = self.model._meta.model_name,
)
self.history_delete_children = History.objects.filter(
item_parent_pk = self.deleted_pk,
item_parent_class = self.item_parent._meta.model_name,
)

View File

@ -10,15 +10,32 @@ import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from access.tests.abstract.model_permissions_organization_manager import OrganizationManagerModelPermissionAdd, OrganizationManagerModelPermissionDelete
from app.tests.abstract.model_permissions import ModelPermissionsAdd, ModelPermissionsChange, ModelPermissionsDelete
class TeamUserPermissions(TestCase):
class TeamUserPermissions(
TestCase,
ModelPermissionsAdd,
ModelPermissionsDelete,
OrganizationManagerModelPermissionAdd,
OrganizationManagerModelPermissionDelete
):
model = TeamUsers
model_name = 'teamusers'
app_label = 'access'
app_namespace = 'Access'
url_name_view = '_team_user_view'
url_name_add = '_team_user_add'
url_name_change = '_team_user_view'
url_name_delete = '_team_user_delete'
@classmethod
def setUpTestData(self):
@ -37,6 +54,8 @@ class TeamUserPermissions(TestCase):
different_organization = Organization.objects.create(name='test_different_organization')
self.different_organization = different_organization
self.test_team = Team.objects.create(
team_name = 'test_team',
organization = organization,
@ -49,11 +68,34 @@ class TeamUserPermissions(TestCase):
user = self.team_user
)
self.url_view_kwargs = {'pk': self.item.id}
self.url_add_kwargs = {'organization_id': self.organization.id, 'pk': self.item.id}
self.add_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
self.url_change_kwargs = {'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id}
self.change_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
self.url_delete_kwargs = {'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id}
self.delete_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
self.url_delete_response = reverse('Access:_team_view',
kwargs={
'organization_id': self.organization.id,
'pk': self.test_team.id
}
)
view_permissions = Permission.objects.get(
codename = 'view_' + self.model_name,
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -67,10 +109,10 @@ class TeamUserPermissions(TestCase):
add_permissions = Permission.objects.get(
codename = 'add_' + self.model_name,
codename = 'add_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -84,10 +126,10 @@ class TeamUserPermissions(TestCase):
change_permissions = Permission.objects.get(
codename = 'change_' + self.model_name,
codename = 'change_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -101,10 +143,10 @@ class TeamUserPermissions(TestCase):
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model_name,
codename = 'delete_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -164,153 +206,21 @@ class TeamUserPermissions(TestCase):
user = self.different_organization_user
)
self.user_is_organization_manager = User.objects.create_user(
username="test_org_manager",
password="password"
)
self.organization.manager = self.user_is_organization_manager
self.organization.save()
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_view_user_anon_denied(self):
""" Check correct permission for view
self.different_organization_is_manager = User.objects.create_user(
username="test_org_manager_different_org",
password="password"
)
Attempt to view as anon user
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
response = client.get(url)
assert response.status_code == 302 and response.url.startswith('/account/login')
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_view_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_view_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
@pytest.mark.skip(reason="feature does not exist")
def test_team_user_auth_view_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200
def test_team_user_auth_add_user_anon_denied(self):
""" Check correct permission for add
Attempt to add as anon user
"""
client = Client()
url = reverse('Access:_team_user_add', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
response = client.put(url, data={'device': 'device'})
assert response.status_code == 302 and response.url.startswith('/account/login')
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_team_user_auth_add_no_permission_denied(self):
""" Check correct permission for add
Attempt to add as user with no permissions
"""
client = Client()
url = reverse('Access:_team_user_add', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_team_user_auth_add_different_organization_denied(self):
""" Check correct permission for add
attempt to add as user from different organization
"""
client = Client()
url = reverse('Access:_team_user_add', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.post(url, data={'name': 'device', 'organization': self.organization.id})
assert response.status_code == 403
def test_team_user_auth_add_permission_view_denied(self):
""" Check correct permission for add
Attempt to add a user with view permission
"""
client = Client()
url = reverse('Access:_team_user_add', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.view_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_team_user_auth_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with no permission
"""
client = Client()
url = reverse('Access:_team_user_add', kwargs={'organization_id': self.organization.id, 'pk': self.item.id})
client.force_login(self.add_user)
response = client.post(url, data={'device': 'device', 'organization': self.organization.id})
assert response.status_code == 200
self.different_organization.manager = self.different_organization_is_manager
self.different_organization.save()
@ -413,126 +323,3 @@ class TeamUserPermissions(TestCase):
response = client.post(url, data={'device': 'device'})
assert response.status_code == 200
def test_team_user_auth_delete_user_anon_denied(self):
""" Check correct permission for delete
Attempt to delete item as anon user
"""
client = Client()
url = reverse('Access:_team_user_delete', kwargs={'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id})
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_team_user_auth_delete_no_permission_denied(self):
""" Check correct permission for delete
Attempt to delete as user with no permissons
"""
client = Client()
url = reverse('Access:_team_user_delete', kwargs={'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_team_user_auth_delete_different_organization_denied(self):
""" Check correct permission for delete
Attempt to delete as user from different organization
"""
client = Client()
url = reverse('Access:_team_user_delete', kwargs={'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_team_user_auth_delete_permission_view_denied(self):
""" Check correct permission for delete
Attempt to delete as user with veiw permission only
"""
client = Client()
url = reverse('Access:_team_user_delete', kwargs={'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id})
client.force_login(self.view_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_team_user_auth_delete_permission_add_denied(self):
""" Check correct permission for delete
Attempt to delete as user with add permission only
"""
client = Client()
url = reverse('Access:_team_user_delete', kwargs={'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id})
client.force_login(self.add_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_team_user_auth_delete_permission_change_denied(self):
""" Check correct permission for delete
Attempt to delete as user with change permission only
"""
client = Client()
url = reverse('Access:_team_user_delete', kwargs={'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id})
client.force_login(self.change_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_team_user_auth_delete_has_permission(self):
""" Check correct permission for delete
Delete item as user with delete permission
"""
client = Client()
url = reverse('Access:_team_user_delete',
kwargs={
'organization_id': self.organization.id,
'team_id': self.test_team.id,
'pk': self.item.id
}
)
client.force_login(self.delete_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 302 and response.url == reverse('Access:_team_view',
kwargs={
'organization_id': self.organization.id,
'pk': self.test_team.id
}
)

View File

@ -1,32 +1,15 @@
# from django.conf import settings
# from django.shortcuts import reverse
from django.test import TestCase, Client
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization, Team, TeamUsers, Permission
@pytest.mark.skip(reason="to be written")
def test_team_user_auth_view_api(user):
""" Check correct permission for view """
pass
from api.tests.abstract.api_permissions import APIPermissions
@pytest.mark.skip(reason="to be written")
def test_team_user_auth_add_api(user):
""" Check correct permission for add """
pass
class TeamUsersPermissionsAPI(TestCase, APIPermissions):
@pytest.mark.skip(reason="to be written")
def test_team_user_auth_change_api(user):
""" Check correct permission for change """
pass
@pytest.mark.skip(reason="to be written")
def test_team_user_auth_delete_api(user):
""" Check correct permission for delete """
pass
model = TeamUsers

View File

@ -0,0 +1,24 @@
import pytest
import unittest
from django.test import TestCase
class TenancyObject(TestCase):
# @classmethod
# def setUpTestData(self):
# """ Setup Test """
# pass
@pytest.mark.skip(reason="to be written")
def test_function_save_attributes(self):
""" Ensure save Attributes function match django default
the save method is overridden. the function attributes must match default django method
"""
pass

View File

@ -5,6 +5,8 @@ from django.views import generic
from access.mixin import *
from access.models import *
from access.forms.organization import OrganizationForm
class IndexView(OrganizationPermission, generic.ListView):
@ -28,13 +30,26 @@ class IndexView(OrganizationPermission, generic.ListView):
class View(OrganizationPermission, generic.UpdateView):
context_object_name = "organization"
form_class = OrganizationForm
model = Organization
permission_required = [
'access.view_organization',
'access.change_organization',
]
template_name = "access/organization.html.j2"
fields = ["name", 'id']
def get(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if not self.permission_check(request, [ 'access.view_organization' ]):
raise PermissionDenied('You are not part of this organization')
return super().get(request, *args, **kwargs)
def get_success_url(self, **kwargs):
@ -48,7 +63,7 @@ class View(OrganizationPermission, generic.UpdateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['organization'] = Organization.objects.get(pk=self.kwargs['pk'])
context['model_docs_path'] = self.model._meta.app_label + '/' + self.model._meta.model_name + '/'
context['teams'] = Team.objects.filter(organization=self.kwargs['pk'])
@ -58,9 +73,16 @@ class View(OrganizationPermission, generic.UpdateView):
return context
@method_decorator(auth_decorator.permission_required("access.change_organization", raise_exception=True))
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if not self.permission_check(request, [ 'access.change_organization' ]):
raise PermissionDenied('You are not part of this organization')
return super().post(request, *args, **kwargs)

View File

@ -4,30 +4,48 @@ from django.utils.decorators import method_decorator
from django.urls import reverse
from django.views import generic
from access.forms.team import TeamForm
# from access.forms.team_users import TeamUsersForm
from access.models import Team, TeamUsers, Organization
from access.mixin import *
class View(OrganizationPermission, generic.UpdateView):
context_object_name = "team"
form_class = TeamForm
model = Team
permission_required = [
'access.view_team',
'access.change_team',
]
template_name = 'access/team.html.j2'
fields = [
"name",
'id',
'organization',
'permissions'
]
def get(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if not self.permission_check(request, [ 'access.view_team' ]):
raise PermissionDenied('You are not part of this organization')
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_docs_path'] = self.model._meta.app_label + '/' + self.model._meta.model_name + '/'
organization = Organization.objects.get(pk=self.kwargs['organization_id'])
context['organization'] = organization
@ -37,7 +55,6 @@ class View(OrganizationPermission, generic.UpdateView):
teamusers = TeamUsers.objects.filter(team=self.kwargs['pk'])
context['teamusers'] = teamusers
context['permissions'] = Permission.objects.filter()
context['model_pk'] = self.kwargs['pk']
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
@ -48,9 +65,16 @@ class View(OrganizationPermission, generic.UpdateView):
return reverse('Access:_team_view', args=(self.kwargs['organization_id'], self.kwargs['pk'],))
@method_decorator(auth_decorator.permission_required("access.change_team", raise_exception=True))
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if not self.permission_check(request, [ 'access.change_team' ]):
raise PermissionDenied('You are not part of this organization')
return super().post(request, *args, **kwargs)

View File

@ -3,22 +3,28 @@ from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import generic
from access.forms.team_users import TeamUsersForm
from access.mixin import OrganizationPermission
from access.models import Team, TeamUsers
class Add(OrganizationPermission, generic.CreateView):
context_object_name = "teamuser"
form_class = TeamUsersForm
model = TeamUsers
parent_model = TeamUsers
permission_required = [
'access.view_team',
'access.add_teamusers'
]
template_name = 'form.html.j2'
fields = [
'user',
'manager'
]
def form_valid(self, form):

View File

@ -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)

View File

View File

View File

@ -0,0 +1,470 @@
import pytest
import unittest
from django.shortcuts import reverse
from django.test import TestCase, Client
class APIPermissionView:
model: object
""" Item Model to test """
app_namespace: str = None
""" URL namespace """
url_name: str
""" URL name of the view to test """
url_view_kwargs: dict = None
""" URL kwargs of the item page """
def test_view_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
response = client.get(url)
assert response.status_code == 401
def test_view_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
def test_view_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
def test_view_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200
class APIPermissionAdd:
model: object
""" Item Model to test """
app_namespace: str = None
""" URL namespace """
url_list: str
""" URL view name of the item list page """
url_kwargs: dict = None
""" URL view kwargs for the item list page """
add_data: dict = None
def test_add_user_anon_denied(self):
""" Check correct permission for add
Attempt to add as anon user
"""
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_list, kwargs = self.url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_list)
response = client.put(url, data=self.add_data)
assert response.status_code == 401
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_add_no_permission_denied(self):
""" Check correct permission for add
Attempt to add as user with no permissions
"""
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_list, kwargs = self.url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_list)
client.force_login(self.no_permissions_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_add_different_organization_denied(self):
""" Check correct permission for add
attempt to add as user from different organization
"""
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_list, kwargs = self.url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_list)
client.force_login(self.different_organization_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
def test_add_permission_view_denied(self):
""" Check correct permission for add
Attempt to add a user with view permission
"""
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_list, kwargs = self.url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_list)
client.force_login(self.view_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
def test_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with no permission
"""
client = Client()
if self.url_kwargs:
url = reverse(self.app_namespace + ':' + self.url_list, kwargs = self.url_kwargs)
else:
url = reverse(self.app_namespace + ':' + self.url_list)
client.force_login(self.add_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 201
class APIPermissionChange:
model: object
""" Item Model to test """
app_namespace: str = None
""" URL namespace """
url_name: str
""" URL name of the view to test """
url_view_kwargs: dict = None
""" URL kwargs of the item page """
change_data: dict = None
def test_change_user_anon_denied(self):
""" Check correct permission for change
Attempt to change as anon
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 401
def test_change_no_permission_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user without permissions
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.no_permissions_user)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 403
def test_change_different_organization_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.different_organization_user)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 403
def test_change_permission_view_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with view permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 403
def test_change_permission_add_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with add permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.add_user)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 403
def test_change_has_permission(self):
""" Check correct permission for change
Make change with user who has change permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.change_user)
response = client.patch(url, data=self.change_data, content_type='application/json')
assert response.status_code == 200
class APIPermissionDelete:
model: object
""" Item Model to test """
app_namespace: str = None
""" URL namespace """
url_name: str
""" URL name of the view to test """
url_view_kwargs: dict = None
""" URL kwargs of the item page """
delete_data: dict = None
def test_delete_user_anon_denied(self):
""" Check correct permission for delete
Attempt to delete item as anon user
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 401
def test_delete_no_permission_denied(self):
""" Check correct permission for delete
Attempt to delete as user with no permissons
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.no_permissions_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_delete_different_organization_denied(self):
""" Check correct permission for delete
Attempt to delete as user from different organization
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.different_organization_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_delete_permission_view_denied(self):
""" Check correct permission for delete
Attempt to delete as user with veiw permission only
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_delete_permission_add_denied(self):
""" Check correct permission for delete
Attempt to delete as user with add permission only
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.add_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_delete_permission_change_denied(self):
""" Check correct permission for delete
Attempt to delete as user with change permission only
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.change_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_delete_has_permission(self):
""" Check correct permission for delete
Delete item as user with delete permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
client.force_login(self.delete_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 204
class APIPermissions(
APIPermissionAdd,
APIPermissionChange,
APIPermissionDelete,
APIPermissionView
):
""" Abstract class containing all API Permission test cases """
model: object
""" Item Model to test """

View File

@ -1,151 +1,251 @@
from django.shortcuts import reverse
from django.test import TestCase, Client
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
from unittest.mock import patch
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_added():
""" Device is created """
pass
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
@pytest.mark.skip(reason="to be written")
def test_api_inventory_operating_system_added():
""" Operating System is created """
pass
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"
}
]
}
@pytest.mark.skip(reason="to be written")
def test_api_inventory_operating_system_version_added():
""" Operating System version is created """
pass
@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()
@pytest.mark.skip(reason="to be written")
def test_api_inventory_device_has_operating_system_added():
""" Operating System version linked to device """
pass
@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(self):
""" Device is created """
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_operating_system_added(self):
""" Operating System is created """
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_operating_system_version_added(self):
""" Operating System version is created """
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_device_has_operating_system_added(self):
""" Operating System version linked to device """
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_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_category_linked_to_software():
""" Software category linked to software """
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_version_added():
""" Test software version 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_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_added(self):
""" Test software exists """
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_category_linked_to_software(self):
""" Software category linked to software """
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_added(self):
""" Test software version exists """
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_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_device_software_has_installed_date():
""" Inventoried software version has install date """
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_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_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_valid_status_created():
""" Successful inventory upload returns 201 """
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_invalid_status_bad_request():
""" Incorrectly formated inventory upload returns 400 """
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_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_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(self):
""" Successful inventory upload returns 201 """
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(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

View File

@ -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 @@ class InventoryPermissionsAPI(TestCase):
)
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 @@ class InventoryPermissionsAPI(TestCase):
@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 @@ class InventoryPermissionsAPI(TestCase):
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 @@ class InventoryPermissionsAPI(TestCase):
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 @@ class InventoryPermissionsAPI(TestCase):
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 @@ class InventoryPermissionsAPI(TestCase):
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 @@ class InventoryPermissionsAPI(TestCase):
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

View File

@ -2,9 +2,11 @@
import json
import re
from django.http import JsonResponse
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.mixin import OrganizationMixin
from access.models import Organization
from api.views.mixin import OrganizationPermissionAPI
from api.serializers.itam.inventory import InventorySerializer
from core.http.common import Http
@ -36,44 +39,81 @@ class InventoryPermissions(OrganizationPermissionAPI):
class Collect(OrganizationMixin, views.APIView):
permission_classes = [
InventoryPermissions
]
class Collect(OrganizationPermissionAPI, views.APIView):
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)
status = Http.Status.BAD_REQUEST
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 +127,7 @@ class Collect(OrganizationMixin, views.APIView):
operating_system = OperatingSystem.objects.create(
name = data['os']['name'],
organization = default_organization,
organization = self.default_organization,
is_global = True
)
@ -95,7 +135,7 @@ class Collect(OrganizationMixin, views.APIView):
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 +144,7 @@ class Collect(OrganizationMixin, views.APIView):
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 +168,7 @@ class Collect(OrganizationMixin, views.APIView):
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 +263,7 @@ class Collect(OrganizationMixin, views.APIView):
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 +280,7 @@ class Collect(OrganizationMixin, views.APIView):
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 +324,9 @@ class Collect(OrganizationMixin, views.APIView):
device.save()
status = Http.Status.OK
if status != Http.Status.CREATED:
status = Http.Status.OK
except Exception as e:

View File

@ -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 @@ class OrganizationPermissionAPI(DjangoObjectPermissions, OrganizationMixin):
raise ValidationError('you must provide an organization')
object_organization = int(request.data['organization'])
elif method == 'patch':
action = 'change'
@ -126,12 +125,17 @@ class OrganizationPermissionAPI(DjangoObjectPermissions, OrganizationMixin):
return True
if hasattr(self, 'default_organization'):
object_organization = self.default_organization
if object_organization is None:
if method == 'post' and hasattr(self, 'default_organization'):
raise Exception("unable to determine object organization")
if self.default_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

View File

@ -21,6 +21,34 @@ def request(request):
return request.get_full_path()
def social_backends(request):
""" Fetch Backend Names
Required for use on the login page to dynamically build the social auth URLS
Returns:
list(str): backend name
"""
from importlib import import_module
social_backends = []
if hasattr(settings, 'SSO_BACKENDS'):
for backend in settings.SSO_BACKENDS:
paths = str(backend).split('.')
module = import_module(paths[0] + '.' + paths[1] + '.' + paths[2])
backend_class = getattr(module, paths[3])
backend = backend_class.name
social_backends += [ str(backend) ]
return social_backends
def user_settings(context) -> int:
""" Provides the settings ID for the current user.
@ -74,6 +102,7 @@ def nav_items(context) -> list(dict()):
'admin',
'djdt', # Debug application
'api',
'social',
]
nav_items = []
@ -136,5 +165,6 @@ def common(context):
return {
'build_details': build_details(context),
'nav_items': nav_items(context),
'social_backends': social_backends(context),
'user_settings': user_settings(context),
}

View File

@ -0,0 +1,33 @@
def merge_software(software: list, new_software: list) -> list:
""" Merge two lists of software actions
Args:
software (list(dict)): Original list to merge over
new_software (list(dict)): new list to use to merge over
Returns:
list(dict): merged list of software actions
"""
merge_software = []
merge: dict = {}
for original in software:
merge.update({
original['name']: original
})
for new in new_software:
merge.update({
new['name']: new
})
for key, value in merge.items():
merge_software = merge_software + [ value ]
return merge_software

View File

@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/5.0/ref/settings/
"""
import os
import sys
from pathlib import Path
from split_settings.tools import optional, include
@ -23,20 +24,33 @@ SETTINGS_DIR = '/etc/itsm' # Primary Settings Directory
BUILD_REPO = os.getenv('CI_PROJECT_URL')
BUILD_SHA = os.getenv('CI_COMMIT_SHA')
BUILD_VERSION = os.getenv('CI_COMMIT_TAG')
DOCS_ROOT = 'https://nofusscomputing.com/projects/django-template/user/'
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-b*41-$afq0yl)1e#qpz^-nbt-opvjwb#avv++b9rfdxa@b55sk'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
#
# Defaults
#
ALLOWED_HOSTS = [ '*' ] # Site host to serve
DEBUG = False # SECURITY WARNING: don't run with debug turned on in production!
SITE_URL = 'http://127.0.0.1' # domain with HTTP method for the sites URL
SECRET_KEY = None # You need to generate this
SESSION_COOKIE_AGE = 1209600 # Age the session cookie should live for in seconds.
SSO_ENABLED = False # Enable SSO
SSO_LOGIN_ONLY_BACKEND = None # Use specified SSO backend as the ONLY method to login. (builting login form will not be used)
TRUSTED_ORIGINS = [] # list of trusted domains for CSRF
ALLOWED_HOSTS = [ '*' ]
# Application definition
# CSRF_COOKIE_SECURE = True
# SECURE_HSTS_SECONDS = # ToDo: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SECURE_HSTS_SECONDS
# SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # ToDo: https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header
# SECURE_SSL_REDIRECT = True
# SECURE_SSL_HOST = # ToDo: https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-host
# SESSION_COOKIE_SECURE = True
# USE_X_FORWARDED_HOST = True # ToDo: https://docs.djangoproject.com/en/dev/ref/settings/#use-x-forwarded-host
INSTALLED_APPS = [
'django.contrib.admin',
@ -100,7 +114,7 @@ WSGI_APPLICATION = 'app.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
'NAME': str(BASE_DIR / 'db.sqlite3'),
}
}
@ -209,8 +223,33 @@ if API_ENABLED:
}
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 <Your Token>`.
## Token Generation
To generate a token, run `python3 manage.py drf_create_token <username>` from the CLI.
## Examples
curl:
- Simple API Request: `curl -X GET <url>/api/ -H 'Authorization: Token <token>'`
- Post an Inventory File:
``` bash
curl --header "Content-Type: application/json" \\
--header "Authorization: Token <token>" \\
--request POST \\
--data @<path to inventory file>/<file name>.json \\
<url>/api/device/inventory
```
""",
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
@ -220,12 +259,34 @@ if API_ENABLED:
}
DATETIME_FORMAT = 'j N Y H:i:s'
#
# Settings for unit tests
#
RUNNING_TESTS = 'test' in str(sys.argv)
if RUNNING_TESTS:
SECRET_KEY = 'django-insecure-tests_are_being_run'
#
# Load user settings files
#
if os.path.isdir(SETTINGS_DIR):
settings_files = os.path.join(SETTINGS_DIR, '*.py')
include(optional(settings_files))
#
# Settings to reset to prevent user from over-riding
#
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
)
CSRF_TRUSTED_ORIGINS = [
SITE_URL,
*TRUSTED_ORIGINS
]
if DEBUG:
INSTALLED_APPS += [
@ -245,3 +306,25 @@ if DEBUG:
'information.apps.InformationConfig',
'project_management.apps.ProjectManagementConfig',
]
if SSO_ENABLED:
if SSO_LOGIN_ONLY_BACKEND:
LOGIN_URL = f'/sso/login/{SSO_LOGIN_ONLY_BACKEND}/'
AUTHENTICATION_BACKENDS += (
*SSO_BACKENDS,
)
SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'social_core.pipeline.social_auth.associate_by_email',
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
)

View File

View File

View File

@ -0,0 +1,442 @@
import pytest
import unittest
from django.test import Client
from django.shortcuts import reverse
class ModelPermissionsView:
""" Tests for checking model view permissions """
app_namespace: str = None
url_name_view: str
url_view_kwargs: dict = None
def test_model_view_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_view, kwargs=self.url_view_kwargs)
else:
url = reverse(self.url_name_view, kwargs=self.url_view_kwargs)
response = client.get(url)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_model_view_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_view, kwargs=self.url_view_kwargs)
else:
url = reverse(self.url_name_view, kwargs=self.url_view_kwargs)
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
def test_model_view_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_view, kwargs=self.url_view_kwargs)
else:
url = reverse(self.url_name_view, kwargs=self.url_view_kwargs)
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
def test_model_view_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
if self.app_namespace:
url = reverse(self.app_namespace + ':' + self.url_name_view, kwargs=self.url_view_kwargs)
else:
url = reverse(self.url_name_view, kwargs=self.url_view_kwargs)
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200
class ModelPermissionsAdd:
""" Tests for checking model Add permissions """
app_namespace: str = None
url_name_add: str
url_add_kwargs: dict = None
add_data: dict = None
def test_model_add_user_anon_denied(self):
""" Check correct permission for add
Attempt to add as anon user
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
response = client.put(url, data=self.add_data)
assert response.status_code == 302 and response.url.startswith('/account/login')
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_model_add_no_permission_denied(self):
""" Check correct permission for add
Attempt to add as user with no permissions
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
client.force_login(self.no_permissions_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_model_add_different_organization_denied(self):
""" Check correct permission for add
attempt to add as user from different organization
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
client.force_login(self.different_organization_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
def test_model_add_permission_view_denied(self):
""" Check correct permission for add
Attempt to add a user with view permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
client.force_login(self.view_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 403
def test_model_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with no permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
client.force_login(self.add_user)
response = client.post(url, data=self.add_data)
assert response.status_code == 200
class ModelPermissionsChange:
""" Tests for checking model change permissions """
app_namespace: str = None
url_name_change: str
url_change_kwargs: dict = None
change_data: dict = None
def test_model_change_user_anon_denied(self):
""" Check correct permission for change
Attempt to change as anon
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
response = client.patch(url, data=self.change_data)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_model_change_no_permission_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user without permissions
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
client.force_login(self.no_permissions_user)
response = client.post(url, data=self.change_data)
assert response.status_code == 403
def test_model_change_different_organization_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
client.force_login(self.different_organization_user)
response = client.post(url, data=self.change_data)
assert response.status_code == 403
def test_model_change_permission_view_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with view permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
client.force_login(self.view_user)
response = client.post(url, data=self.change_data)
assert response.status_code == 403
def test_model_change_permission_add_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with add permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
client.force_login(self.add_user)
response = client.post(url, data=self.change_data)
assert response.status_code == 403
def test_model_change_has_permission(self):
""" Check correct permission for change
Make change with user who has change permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
client.force_login(self.change_user)
response = client.post(url, data=self.change_data)
assert response.status_code == 200
class ModelPermissionsDelete:
""" Tests for checking model delete permissions """
app_namespace: str = None
url_name_delete: str
url_delete_kwargs: dict = None
url_delete_response: str
delete_data: dict = None
def test_model_delete_user_anon_denied(self):
""" Check correct permission for delete
Attempt to delete item as anon user
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_model_delete_no_permission_denied(self):
""" Check correct permission for delete
Attempt to delete as user with no permissons
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
client.force_login(self.no_permissions_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_model_delete_different_organization_denied(self):
""" Check correct permission for delete
Attempt to delete as user from different organization
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
client.force_login(self.different_organization_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_model_delete_permission_view_denied(self):
""" Check correct permission for delete
Attempt to delete as user with veiw permission only
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
client.force_login(self.view_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_model_delete_permission_add_denied(self):
""" Check correct permission for delete
Attempt to delete as user with add permission only
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
client.force_login(self.add_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_model_delete_permission_change_denied(self):
""" Check correct permission for delete
Attempt to delete as user with change permission only
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
client.force_login(self.change_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 403
def test_model_delete_has_permission(self):
""" Check correct permission for delete
Delete item as user with delete permission
"""
client = Client()
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
client.force_login(self.delete_user)
response = client.delete(url, data=self.delete_data)
assert response.status_code == 302 and response.url == self.url_delete_response
class ModelPermissions(
ModelPermissionsView,
ModelPermissionsAdd,
ModelPermissionsChange,
ModelPermissionsDelete
):
""" Tests for checking model permissions """
app_namespace: str = None

View File

@ -0,0 +1,88 @@
from django.conf import settings as django_settings
from django.shortcuts import reverse
from django.test import TestCase, Client
from app.helpers.merge_software import merge_software
import pytest
import unittest
class MergeSoftwareHelper(TestCase):
""" tests for function `merge_software` """
@classmethod
def setUpTestData(self):
self.data: dict = {
'first_list': [
{
'name': 'software_1',
'state': 'install'
},
{
'name': 'software_2',
'state': 'install'
}
],
'second_list': [
{
'name': 'software_1',
'state': 'absent'
},
{
'name': 'software_2',
'state': 'absent'
}
],
'third_list': [
{
'name': 'software_1',
'state': 'other'
},
{
'name': 'software_2',
'state': 'other'
},
{
'name': 'software_3',
'state': 'install'
}
]
}
self.software_list_one = merge_software(self.data['first_list'], self.data['second_list'])
self.software_list_two = merge_software(self.software_list_one, self.data['third_list'])
def test_merging_0_0(self):
""" ensure Second list overwrites the first app1 """
assert self.software_list_one[0]['state'] == 'absent'
def test_merging_0_1(self):
""" ensure Second list overwrites the first app2 """
assert self.software_list_one[1]['state'] == 'absent'
def test_merging_1_0(self):
""" ensure Second list overwrites the first app1 again """
assert self.software_list_two[0]['state'] == 'other'
def test_merging_1_1(self):
""" ensure Second list overwrites the first app2 again """
assert self.software_list_two[1]['state'] == 'other'
def test_merging_1_new_list_item(self):
""" ensure Second list overwrites the first app2 again """
assert len(self.software_list_two) == 3

View File

@ -43,9 +43,18 @@ urlpatterns = [
path("config_management/", include("config_management.urls")),
path("history/<str:model_name>/<int:model_pk>", history.View.as_view(), name='_history'),
re_path(r'^static/(?P<path>.*)$', serve,{'document_root': settings.STATIC_ROOT})
re_path(r'^static/(?P<path>.*)$', serve,{'document_root': settings.STATIC_ROOT}),
]
if settings.SSO_ENABLED:
urlpatterns += [
path('sso/', include('social_django.urls', namespace='social'))
]
if settings.API_ENABLED:
urlpatterns += [
@ -54,12 +63,14 @@ if settings.API_ENABLED:
path('api/swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
]
if settings.DEBUG:
urlpatterns += [
path("__debug__/", include("debug_toolbar.urls"), name='_debug'),
# Apps Under Development
path("itim/", include("itim.urls")),
path("information/", include("information.urls")),
path("project_management/", include("project_management.urls")),
]

View File

@ -0,0 +1,21 @@
from django import forms
from django.db.models import Q
from config_management.models.groups import ConfigGroupSoftware
from itam.models.software import Software
class SoftwareAdd(forms.ModelForm):
class Meta:
model = ConfigGroupSoftware
fields = [
'software',
'action'
]
def __init__(self, *args, **kwargs):
organizations = kwargs.pop('organizations')
super().__init__(*args, **kwargs)
self.fields['software'].queryset = Software.objects.filter(Q(organization_id__in=organizations) | Q(is_global = True))

View File

@ -0,0 +1,22 @@
from django import forms
from django.db.models import Q
from config_management.models.groups import ConfigGroupSoftware
from itam.models.software import Software, SoftwareVersion
class SoftwareUpdate(forms.ModelForm):
class Meta:
model = ConfigGroupSoftware
fields = [
'action',
'version',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['version'].queryset = SoftwareVersion.objects.filter(software_id=self.instance.software.id)

View File

@ -0,0 +1,17 @@
from django import forms
from django.db.models import Q
from config_management.models.groups import ConfigGroups
from itam.models.software import Software, SoftwareVersion
class ConfigGroupForm(forms.ModelForm):
class Meta:
model = ConfigGroups
fields = [
'name',
'parent',
'is_global',
'config',
]

View File

@ -0,0 +1,36 @@
# Generated by Django 5.0.6 on 2024-06-07 21:43
import access.fields
import access.models
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('access', '0003_alter_team_organization'),
('config_management', '0003_alter_configgrouphosts_organization_and_more'),
('itam', '0013_alter_device_organization_and_more'),
]
operations = [
migrations.CreateModel(
name='ConfigGroupSoftware',
fields=[
('is_global', models.BooleanField(default=False)),
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
('action', models.CharField(blank=True, choices=[('1', 'Install'), ('0', 'Remove')], default=None, max_length=1, null=True)),
('config_group', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups')),
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
('software', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='itam.software')),
('version', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.softwareversion')),
],
options={
'ordering': ['-action', 'software'],
},
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 5.0.6 on 2024-06-11 20:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config_management', '0004_configgroupsoftware'),
]
operations = [
migrations.AddField(
model_name='configgrouphosts',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='configgroups',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='configgroupsoftware',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
]

View File

@ -7,9 +7,12 @@ from django.forms import ValidationError
from access.fields import *
from access.models import TenancyObject
from app.helpers.merge_software import merge_software
from core.mixin.history_save import SaveHistory
from itam.models.device import Device
from itam.models.device import Device, DeviceSoftware
from itam.models.software import Software, SoftwareVersion
@ -113,6 +116,14 @@ class ConfigGroups(GroupsCommonFields, SaveHistory):
@property
def parent_object(self):
""" Fetch the parent object """
return self.parent
def render_config(self) -> str:
config: dict = dict()
@ -125,14 +136,49 @@ class ConfigGroups(GroupsCommonFields, SaveHistory):
config.update(self.config)
softwares = ConfigGroupSoftware.objects.filter(config_group=self.id)
software_actions = {
"software": []
}
for software in softwares:
if software.action:
if int(software.action) == 1:
state = 'present'
elif int(software.action) == 0:
state = 'absent'
software_action = {
"name": software.software.slug,
"state": state
}
if software.version:
software_action['version'] = software.version.name
software_actions['software'] = software_actions['software'] + [ software_action ]
if len(software_actions['software']) > 0: # don't add empty software as it prevents parent software from being added
if 'software' not in config.keys():
config['software'] = []
config['software'] = merge_software(config['software'], software_actions['software'])
return json.dumps(config)
def save(self, *args, **kwargs):
self.is_global = False
if self.config:
self.config = self.config_keys_ansible_variable(self.config)
@ -142,8 +188,13 @@ class ConfigGroups(GroupsCommonFields, SaveHistory):
super().save(*args, **kwargs)
def __str__(self):
if self.parent:
return f'{self.parent.name} > {self.name}'
return self.name
@ -177,3 +228,66 @@ class ConfigGroupHosts(GroupsCommonFields, SaveHistory):
null = False,
blank= False
)
@property
def parent_object(self):
""" Fetch the parent object """
return self.group
class ConfigGroupSoftware(GroupsCommonFields, SaveHistory):
""" A way to configure software to install/remove per config group """
class Meta:
ordering = [
'-action',
'software'
]
config_group = models.ForeignKey(
ConfigGroups,
on_delete=models.CASCADE,
default = None,
null = False,
blank= False
)
software = models.ForeignKey(
Software,
on_delete=models.CASCADE,
default = None,
null = False,
blank= False
)
action = models.CharField(
max_length=1,
choices=DeviceSoftware.Actions,
default=None,
null=True,
blank = True,
)
version = models.ForeignKey(
SoftwareVersion,
on_delete=models.CASCADE,
default = None,
null = True,
blank= True
)
@property
def parent_object(self):
""" Fetch the parent object """
return self.config_group

View File

@ -1,6 +1,6 @@
{% extends 'base.html.j2' %}
{% block body %}
{% block content %}
<script>
@ -120,6 +120,45 @@
Software
</h3>
<input type="button" value="Add Software Action" onclick="window.location='{% url 'Config Management:_group_software_add' model_pk %}';">
<table>
<thead>
<th>Name</th>
<th>Category</th>
<th>Action</th>
<th>Desired Version</th>
<th>&nbsp;</th>
</thead>
{% if softwares %}
{% for software in softwares %}
<tr>
<td><a href="{% url 'ITAM:_software_view' pk=software.software_id %}">{{ software.software }}</a></td>
<td>{{ software.software.category }}</td>
<td>
{% url 'Config Management:_group_software_change' group_id=group.id pk=software.id as icon_link %}
{% if software.get_action_display == 'Install' %}
{% include 'icons/success_text.html.j2' with icon_text=software.get_action_display icon_link=icon_link %}
{% elif software.get_action_display == 'Remove'%}
{% include 'icons/cross_text.html.j2' with icon_text=software.get_action_display %}
{% else %}
{% include 'icons/add_link.html.j2' with icon_text='Add' %}
{% endif %}
</td>
<td>
{% if software.version %}
{{ software.version }}
{% else %}
-
{% endif %}
</td>
<td>&nbsp;</td>
</tr>
{% endfor %}
{% else %}
<td colspan="5">Nothing Found</td>
{% endif %}
</table>
</div>
<div id="Configuration" class="tabcontent">

View File

@ -1,6 +1,6 @@
{% extends 'base.html.j2' %}
{% block body %}
{% block content %}
<input type="button" value="New Group" onclick="window.location='{% url 'Config Management:_group_add' %}';">
<table class="data">

View File

@ -0,0 +1,88 @@
import pytest
import unittest
from django.test import TestCase
from access.models import Organization
from config_management.models.groups import ConfigGroups
@pytest.mark.django_db
class ConfigGroupsModel(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
)
def test_config_groups_count_child_groups(self):
""" Test function count_children """
assert self.item.count_children() == 1
def test_config_groups_rendered_config_not_empty(self):
""" Rendered Config must be returned """
assert self.item.config is not None
def test_config_groups_rendered_config_is_dict(self):
""" Rendered Config is a string """
assert type(self.item.render_config()) is str
def test_config_groups_rendered_config_is_correct(self):
""" Rendered Config is correct """
assert self.item.config['key'] == 'one'
def test_config_groups_rendered_config_inheritence_overwrite(self):
""" rendered config from parent group merged correctly """
assert self.second_item.config['key'] == 'two'
def test_config_groups_rendered_config_inheritence_existing_key_present(self):
""" rendered config from parent group merge existing key present
during merge, a key that doesn't exist in the child group that exists in the
parent group should be within the child groups rendered config
"""
assert self.second_item.config['key'] == 'two'
@pytest.mark.skip(reason="to be written")
def test_config_groups_config_keys_valid_ansible_variable():
""" All config keys must be valid ansible variables """
pass

View File

@ -1,20 +0,0 @@
import pytest
import unittest
from django.test import TestCase
from config_management.models.groups import ConfigGroups
class ConfigGroups(TestCase):
model = ConfigGroups
model_name = 'configgroups'
@pytest.mark.skip(reason="to be written")
def test_config_groups_config_keys_valid_ansible_variable():
""" All config keys must be valid ansible variables """
pass

View File

@ -8,98 +8,18 @@ from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from core.tests.abstract.history_entry import HistoryEntry
from core.tests.abstract.history_entry_child_model import HistoryEntryChildItem
from config_management.models.groups import ConfigGroups
# @pytest.mark.skip(reason="to be written")
# def test_history_auth_view():
# """ User requires Permission view_history """
# pass
class ConfigGroupsHistory(TestCase, HistoryEntry, HistoryEntryChildItem):
# @pytest.mark.skip(reason="to be written")
# def test_history_device_create():
# """ History row must be added to history table on create """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_update():
# """ History row must be added to history table on updatej """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_delete():
# """ History row must be added to history table on delete """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_operating_system_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_create():
# """ History row must be added to history table on create
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_update():
# """ History row must be added to history table on update
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
# @pytest.mark.skip(reason="to be written")
# def test_history_device_software_delete():
# """ History row must be added to history table on delete
# Must also have populated parent_item_pk and parent_item_class columns
# """
# pass
class ConfigGroupsHistory(TestCase):
model = ConfigGroups
model_name = 'configgroups'
@classmethod
def setUpTestData(self):
@ -109,11 +29,17 @@ class ConfigGroupsHistory(TestCase):
self.organization = organization
self.item_create = self.model.objects.create(
name = 'test_item_' + self.model_name,
self.item_parent = self.model.objects.create(
name = 'test_item_parent_' + self.model._meta.model_name,
organization = self.organization
)
self.item_create = self.model.objects.create(
name = 'test_item_' + self.model._meta.model_name,
organization = self.organization,
parent = self.item_parent
)
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
@ -122,114 +48,34 @@ class ConfigGroupsHistory(TestCase):
)
self.item_change = self.item_create
self.item_change.name = 'test_item_' + self.model_name + '_changed'
self.item_change.name = 'test_item_' + self.model._meta.model_name + '_changed'
self.item_change.save()
self.field_after_expected_value = '{"name": "' + self.item_change.name + '"}'
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
self.item_delete = self.model.objects.create(
name = 'test_item_delete_' + self.model._meta.model_name,
organization = self.organization,
parent = self.item_parent
)
# field type testing to be done as part of model testing
def test_device_history_entry_item_add_field_action(self):
""" Ensure action is "add" for item creation """
self.deleted_pk = self.item_delete.pk
history = self.history_create.__dict__
assert history['action'] == int(History.Actions.ADD[0])
# assert type(history['action']) is int
@pytest.mark.skip(reason="to be written")
def test_device_history_entry_item_add_field_after(self):
""" Ensure after field contains correct value """
history = self.history_create.__dict__
assert history['after'] == str('{}')
# assert type(history['after']) is str
def test_device_history_entry_item_add_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_create.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_device_history_entry_item_add_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_create.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_device_history_entry_item_add_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_create.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
################################## Change ##################################
# field type testing to be done as part of model testing
def test_device_history_entry_item_change_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_change.__dict__
assert history['action'] == int(History.Actions.UPDATE[0])
# assert type(history['action']) is int
def test_device_history_entry_item_change_field_after(self):
""" Ensure after field contains correct value """
history = self.history_change.__dict__
assert history['after'] == str('{"name": "test_item_' + self.model_name + '_changed"}')
# assert type(history['after']) is str
@pytest.mark.skip(reason="to be written")
def test_device_history_entry_item_change_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_change.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_device_history_entry_item_change_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_change.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_device_history_entry_item_change_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_change.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
self.item_delete.delete()
self.history_delete = History.objects.get(
action = History.Actions.DELETE[0],
item_pk = self.deleted_pk,
item_class = self.model._meta.model_name,
)
self.history_delete_children = History.objects.filter(
item_parent_pk = self.deleted_pk,
item_parent_class = self.item_parent._meta.model_name,
)

View File

@ -10,15 +10,29 @@ import unittest
import requests
from access.models import Organization, Team, TeamUsers, Permission
from app.tests.abstract.model_permissions import ModelPermissions
from config_management.models.groups import ConfigGroups
class ConfigGroupPermissions(TestCase):
class ConfigGroupPermissions(TestCase, ModelPermissions):
model = ConfigGroups
model_name = 'configgroups'
app_label = 'config_management'
app_namespace = 'Config Management'
url_name_view = '_group_view'
url_name_add = '_group_add'
url_name_change = '_group_view'
url_name_delete = '_group_delete'
url_delete_response = reverse('Config Management:Groups')
@classmethod
def setUpTestData(self):
@ -43,11 +57,27 @@ class ConfigGroupPermissions(TestCase):
name = 'deviceone'
)
self.url_view_kwargs = {'pk': self.item.id}
# self.url_add_kwargs = {'pk': self.item.id}
self.add_data = {'device': 'device', 'organization': self.organization.id}
self.url_change_kwargs = {'pk': self.item.id}
self.change_data = {'device': 'device', 'organization': self.organization.id}
self.url_delete_kwargs = {'pk': self.item.id}
self.delete_data = {'device': 'device', 'organization': self.organization.id}
view_permissions = Permission.objects.get(
codename = 'view_' + self.model_name,
codename = 'view_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -61,10 +91,10 @@ class ConfigGroupPermissions(TestCase):
add_permissions = Permission.objects.get(
codename = 'add_' + self.model_name,
codename = 'add_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -78,10 +108,10 @@ class ConfigGroupPermissions(TestCase):
change_permissions = Permission.objects.get(
codename = 'change_' + self.model_name,
codename = 'change_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -95,10 +125,10 @@ class ConfigGroupPermissions(TestCase):
delete_permissions = Permission.objects.get(
codename = 'delete_' + self.model_name,
codename = 'delete_' + self.model._meta.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.model_name,
app_label = self.model._meta.app_label,
model = self.model._meta.model_name,
)
)
@ -157,355 +187,3 @@ class ConfigGroupPermissions(TestCase):
team = different_organization_team,
user = self.different_organization_user
)
def test_config_groups_auth_view_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
response = client.get(url)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_config_groups_auth_view_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
def test_config_groups_auth_view_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
def test_config_groups_auth_view_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200
def test_config_groups_auth_add_user_anon_denied(self):
""" Check correct permission for add
Attempt to add as anon user
"""
client = Client()
url = reverse('Config Management:_group_add')
response = client.put(url, data={'device': 'device'})
assert response.status_code == 302 and response.url.startswith('/account/login')
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_config_groups_auth_add_no_permission_denied(self):
""" Check correct permission for add
Attempt to add as user with no permissions
"""
client = Client()
url = reverse('Config Management:_group_add')
client.force_login(self.no_permissions_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
# @pytest.mark.skip(reason="ToDO: figure out why fails")
def test_config_groups_auth_add_different_organization_denied(self):
""" Check correct permission for add
attempt to add as user from different organization
"""
client = Client()
url = reverse('Config Management:_group_add')
client.force_login(self.different_organization_user)
response = client.post(url, data={'name': 'device', 'organization': self.organization.id})
assert response.status_code == 403
def test_config_groups_auth_add_permission_view_denied(self):
""" Check correct permission for add
Attempt to add a user with view permission
"""
client = Client()
url = reverse('Config Management:_group_add')
client.force_login(self.view_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_add_has_permission(self):
""" Check correct permission for add
Attempt to add as user with no permission
"""
client = Client()
url = reverse('Config Management:_group_add')
client.force_login(self.add_user)
response = client.post(url, data={'device': 'device', 'organization': self.organization.id})
assert response.status_code == 200
def test_config_groups_auth_change_user_anon_denied(self):
""" Check correct permission for change
Attempt to change as anon
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
response = client.patch(url, data={'device': 'device'})
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_config_groups_auth_change_no_permission_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user without permissions
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_change_different_organization_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user from different organization
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_change_permission_view_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with view permission
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_change_permission_add_denied(self):
""" Ensure permission view cant make change
Attempt to make change as user with add permission
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.add_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_change_has_permission(self):
""" Check correct permission for change
Make change with user who has change permission
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.change_user)
response = client.post(url, data={'device': 'device'})
assert response.status_code == 200
def test_config_groups_auth_delete_user_anon_denied(self):
""" Check correct permission for delete
Attempt to delete item as anon user
"""
client = Client()
url = reverse('Config Management:_group_delete', kwargs={'pk': self.item.id})
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_config_groups_auth_delete_no_permission_denied(self):
""" Check correct permission for delete
Attempt to delete as user with no permissons
"""
client = Client()
url = reverse('Config Management:_group_delete', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_delete_different_organization_denied(self):
""" Check correct permission for delete
Attempt to delete as user from different organization
"""
client = Client()
url = reverse('Config Management:_group_delete', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_delete_permission_view_denied(self):
""" Check correct permission for delete
Attempt to delete as user with veiw permission only
"""
client = Client()
url = reverse('Config Management:_group_delete', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_delete_permission_add_denied(self):
""" Check correct permission for delete
Attempt to delete as user with add permission only
"""
client = Client()
url = reverse('Config Management:_group_delete', kwargs={'pk': self.item.id})
client.force_login(self.add_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_delete_permission_change_denied(self):
""" Check correct permission for delete
Attempt to delete as user with change permission only
"""
client = Client()
url = reverse('Config Management:_group_delete', kwargs={'pk': self.item.id})
client.force_login(self.change_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 403
def test_config_groups_auth_delete_has_permission(self):
""" Check correct permission for delete
Delete item as user with delete permission
"""
client = Client()
url = reverse('Config Management:_group_delete', kwargs={'pk': self.item.id})
client.force_login(self.delete_user)
response = client.delete(url, data={'device': 'device'})
assert response.status_code == 302 and response.url == reverse('Config Management:Groups')

View File

@ -0,0 +1,63 @@
import pytest
import unittest
from django.test import TestCase
from access.models import Organization
from config_management.models.groups import ConfigGroups, ConfigGroupSoftware
from itam.models.device import DeviceSoftware
from itam.models.software import Software
class ConfigGroupSoftwareModel(TestCase):
model = ConfigGroupSoftware
@classmethod
def setUpTestData(self):
""" Setup Test
"""
organization = Organization.objects.create(name='test_org')
self.parent_item = ConfigGroups.objects.create(
organization=organization,
name = 'group_one'
)
self.software_item = Software.objects.create(
organization=organization,
name = 'softwareone',
)
self.item = self.model.objects.create(
organization = organization,
software = self.software_item,
config_group = self.parent_item,
action = DeviceSoftware.Actions.INSTALL
)
def test_model_has_property_parent_object(self):
""" Check if model contains 'parent_object'
This is a required property for all models that have a parent
"""
assert hasattr(self.model, 'parent_object')
def test_model_property_parent_object_returns_object(self):
""" Check if model contains 'parent_object'
This is a required property for all models that have a parent
"""
assert self.item.parent_object == self.parent_item

View File

@ -0,0 +1,100 @@
import pytest
import unittest
import requests
from django.test import TestCase
from access.models import Organization
from core.models.history import History
from core.tests.abstract.history_entry import HistoryEntry
from core.tests.abstract.history_entry_child_model import HistoryEntryChildItem
from config_management.models.groups import ConfigGroups, ConfigGroupSoftware
from itam.models.device import DeviceSoftware
from itam.models.software import Software
class ConfigGroupSoftwareHistory(TestCase, HistoryEntry, HistoryEntryChildItem):
model = ConfigGroupSoftware
parent_model = ConfigGroups
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_parent = self.parent_model.objects.create(
name = 'test_item_' + self.model._meta.model_name,
organization = self.organization
)
software = Software.objects.create(
name = 'test_item_' + self.model._meta.model_name,
organization = self.organization
)
self.item_create = self.model.objects.create(
organization = self.organization,
config_group = self.item_parent,
software = software,
action = DeviceSoftware.Actions.INSTALL,
)
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name
)
self.item_change = self.item_create
self.item_change.action = DeviceSoftware.Actions.REMOVE
self.item_change.save()
self.field_after_expected_value = '{"action": "' + DeviceSoftware.Actions.REMOVE + '"}'
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
# self.item_delete = self.item_change
#
software_two = Software.objects.create(
name = 'test_item_two_' + self.model._meta.model_name,
organization = self.organization
)
self.item_delete = self.model.objects.create(
organization = self.organization,
config_group = self.item_parent,
software = software_two,
action = DeviceSoftware.Actions.INSTALL,
)
self.deleted_pk = self.item_delete.pk
self.item_delete.delete()
self.history_delete = History.objects.get(
action = History.Actions.DELETE[0],
item_pk = self.deleted_pk,
item_class = self.model._meta.model_name,
)
self.history_delete_children = History.objects.filter(
item_parent_pk = self.deleted_pk,
item_parent_class = self.item_parent._meta.model_name,
)

View File

@ -11,15 +11,15 @@ import requests
from access.models import Organization, Team, TeamUsers, Permission
from config_management.models.groups import ConfigGroups
from core.models.history import History
from core.models.manufacturer import Manufacturer
class ManufacturerHistoryPermissions(TestCase):
class ConfigGroupSoftwaresHistoryPermissions(TestCase):
item_model = Manufacturer
item_model = ConfigGroups
model = History
@ -108,6 +108,7 @@ class ManufacturerHistoryPermissions(TestCase):
@pytest.mark.skip(reason="figure out best way to test")
def test_auth_view_history_user_anon_denied(self):
""" Check correct permission for view
@ -122,6 +123,7 @@ class ManufacturerHistoryPermissions(TestCase):
assert response.status_code == 302 and response.url.startswith('/account/login')
@pytest.mark.skip(reason="figure out best way to test")
def test_auth_view_history_no_permission_denied(self):
""" Check correct permission for view
@ -138,6 +140,7 @@ class ManufacturerHistoryPermissions(TestCase):
assert response.status_code == 403
@pytest.mark.skip(reason="figure out best way to test")
def test_auth_view_history_different_organizaiton_denied(self):
""" Check correct permission for view
@ -154,6 +157,7 @@ class ManufacturerHistoryPermissions(TestCase):
assert response.status_code == 403
@pytest.mark.skip(reason="figure out best way to test")
def test_auth_view_history_has_permission(self):
""" Check correct permission for view

View File

@ -0,0 +1,286 @@
# from django.conf import settings
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 app.tests.abstract.model_permissions import ModelPermissionsAdd, ModelPermissionsChange, ModelPermissionsDelete
from config_management.models.groups import ConfigGroups, ConfigGroupSoftware
from itam.models.device import DeviceSoftware
from itam.models.software import Software
class ConfigGroupSoftwarePermissions(TestCase, ModelPermissionsAdd, ModelPermissionsChange, ModelPermissionsDelete):
model = ConfigGroupSoftware
parent_model = ConfigGroups
model_name = 'configgroupsoftware'
app_label = 'config_management'
app_namespace = 'Config Management'
url_name_view = '_group_view'
url_name_add = '_group_software_add'
url_name_change = '_group_software_change'
url_name_delete = '_group_software_delete'
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
2. create an organization that is different to item
3. Create the parent item
4. create a software item
5. create the item
6. create teams with each permission: view, add, change, delete
7. 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.parent_item = self.parent_model.objects.create(
organization=organization,
name = 'group_one'
)
self.software_item = Software.objects.create(
organization=organization,
name = 'softwareone',
)
self.item = self.model.objects.create(
organization = organization,
software = self.software_item,
config_group = self.parent_item,
action = DeviceSoftware.Actions.INSTALL
)
self.url_view_kwargs = {'pk': self.item.id}
self.url_add_kwargs = {'pk': self.parent_item.id,}
self.add_data = {'device': 'device', 'organization': self.organization.id}
self.url_change_kwargs = {'pk': self.item.id, 'group_id': self.parent_item.id}
self.change_data = {'device': 'device', 'organization': self.organization.id}
self.url_delete_kwargs = {'pk': self.item.id, 'group_id': self.parent_item.id}
self.delete_data = {'device': 'device', 'organization': self.organization.id}
self.url_delete_response = reverse('Config Management:_group_view', kwargs={'pk': self.parent_item.id})
view_permissions = Permission.objects.get(
codename = 'view_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.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_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])
change_permissions = Permission.objects.get(
codename = 'change_' + self.model_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.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_name,
content_type = ContentType.objects.get(
app_label = self.app_label,
model = self.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
)
@pytest.mark.skip(reason="figure out best way to test")
def test_config_groups_auth_view_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
response = client.get(url)
assert response.status_code == 302 and response.url.startswith('/account/login')
@pytest.mark.skip(reason="figure out best way to test")
def test_config_groups_auth_view_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
@pytest.mark.skip(reason="figure out best way to test")
def test_config_groups_auth_view_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
@pytest.mark.skip(reason="figure out best way to test")
def test_config_groups_auth_view_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse('Config Management:_group_view', kwargs={'pk': self.item.id})
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200
@pytest.mark.skip(reason="ToDO: refactor abstract test case")
def test_model_change_has_permission(self):
# this test requires re-write for 302 redirection
# actual test
# assert response.status_code == 302 and response.url == reverse('Config Management:_group_view', kwargs={'pk': self.parent_item.id})
pass

View File

@ -1,6 +1,7 @@
from django.urls import path
from config_management.views.groups import GroupIndexView, GroupAdd, GroupDelete, GroupView, GroupHostAdd, GroupHostDelete
from config_management.views.groups.groups import GroupIndexView, GroupAdd, GroupDelete, GroupView, GroupHostAdd, GroupHostDelete
from config_management.views.groups.software import GroupSoftwareAdd, GroupSoftwareChange, GroupSoftwareDelete
app_name = "Config Management"
@ -8,9 +9,14 @@ urlpatterns = [
path('group', GroupIndexView.as_view(), name='Groups'),
path('group/add', GroupAdd.as_view(), name='_group_add'),
path('group/<int:pk>', GroupView.as_view(), name='_group_view'),
path('group/<int:group_id>/child', GroupAdd.as_view(), name='_group_add_child'),
path('group/<int:pk>/delete', GroupDelete.as_view(), name='_group_delete'),
path("group/<int:pk>/software/add", GroupSoftwareAdd.as_view(), name="_group_software_add"),
path("group/<int:group_id>/software/<int:pk>", GroupSoftwareChange.as_view(), name="_group_software_change"),
path("group/<int:group_id>/software/<int:pk>/delete", GroupSoftwareDelete.as_view(), name="_group_software_delete"),
path('group/<int:group_id>/host', GroupHostAdd.as_view(), name='_group_add_host'),
path('group/<int:group_id>/host/<int:pk>/delete', GroupHostDelete.as_view(), name='_group_delete_host'),

View File

@ -16,7 +16,8 @@ from itam.models.device import Device
from settings.models.user_settings import UserSettings
from config_management.forms.group_hosts import ConfigGroupHostsForm
from config_management.models.groups import ConfigGroups, ConfigGroupHosts
from config_management.forms.group.group import ConfigGroupForm
from config_management.models.groups import ConfigGroups, ConfigGroupHosts, ConfigGroupSoftware
@ -37,6 +38,8 @@ class GroupIndexView(OrganizationPermission, generic.ListView):
context = super().get_context_data(**kwargs)
context['model_docs_path'] = self.model._meta.app_label + '/'
context['content_title'] = 'Config Groups'
return context
@ -112,6 +115,8 @@ class GroupView(OrganizationPermission, generic.UpdateView):
context_object_name = "group"
form_class = ConfigGroupForm
model = ConfigGroups
permission_required = [
@ -121,12 +126,6 @@ class GroupView(OrganizationPermission, generic.UpdateView):
template_name = 'config_management/group.html.j2'
fields = [
'name',
'parent',
'config',
]
def get_context_data(self, **kwargs):
@ -146,6 +145,9 @@ class GroupView(OrganizationPermission, generic.UpdateView):
context['model_delete_url'] = reverse('Config Management:_group_delete', args=(self.kwargs['pk'],))
softwares = ConfigGroupSoftware.objects.filter(config_group=self.kwargs['pk'])[:50]
context['softwares'] = softwares
context['content_title'] = self.object.name
# if self.request.user.is_superuser:
@ -254,9 +256,20 @@ class GroupHostAdd(OrganizationPermission, generic.CreateView):
exsting_group_hosts = ConfigGroupHosts.objects.filter(group=group)
form_class.fields["host"].queryset = Device.objects.filter(
organization=group.organization.id,
).exclude(id__in=exsting_group_hosts.values_list('host', flat=True))
form_class.fields["host"].queryset = None
if group.is_global:
form_class.fields["host"].queryset = Device.objects.filter(
).exclude(
id__in=exsting_group_hosts.values_list('host', flat=True)
)
if form_class.fields["host"].queryset is None:
form_class.fields["host"].queryset = Device.objects.filter(
organization=group.organization.id,
).exclude(id__in=exsting_group_hosts.values_list('host', flat=True))
return form_class

View File

@ -0,0 +1,137 @@
from django.urls import reverse
from django.views import generic
from access.mixin import OrganizationPermission
from itam.models.software import Software
from config_management.forms.group.add_software import SoftwareAdd
from config_management.forms.group.change_software import SoftwareUpdate
from config_management.models.groups import ConfigGroups, ConfigGroupSoftware
class GroupSoftwareAdd(OrganizationPermission, generic.CreateView):
form_class = SoftwareAdd
model = ConfigGroupSoftware
parent_model = ConfigGroups
permission_required = [
'config_management.add_configgroupsoftware',
]
template_name = 'form.html.j2'
def form_valid(self, form):
config_group = ConfigGroups.objects.get(pk=self.kwargs['pk'])
form.instance.organization_id = config_group.organization.id
form.instance.config_group = config_group
software = Software.objects.get(pk=form.instance.software.id)
if ConfigGroupSoftware.objects.filter(
config_group=config_group,
software=software
).exists():
existing_object = ConfigGroupSoftware.objects.get(
device=device,
software=software
)
existing_object.action = form.instance.action
existing_object.save()
return HttpResponseRedirect(self.get_success_url())
else:
return super().form_valid(form)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
obj = ConfigGroups.objects.get(pk=self.kwargs['pk'])
kwargs['organizations'] = [ obj.organization.id ]
return kwargs
def get_success_url(self, **kwargs):
return reverse('Config Management:_group_view', args=(self.kwargs['pk'],))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Add Software Action'
return context
class GroupSoftwareChange(OrganizationPermission, generic.UpdateView):
form_class = SoftwareUpdate
model = ConfigGroupSoftware
permission_required = [
'config_management.change_configgroupsoftware'
]
template_name = 'form.html.j2'
def form_valid(self, form):
config_group = ConfigGroups.objects.get(pk=self.kwargs['group_id'])
form.instance.organization_id = config_group.organization.id
form.instance.config_group = config_group
return super().form_valid(form)
def get_success_url(self, **kwargs):
return reverse('Config Management:_group_view', args=(self.kwargs['group_id'],))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['model_delete_url'] = reverse('Config Management:_group_software_delete', args=(self.kwargs['group_id'], self.kwargs['pk'],))
context['content_title'] = 'Edit Software Action'
return context
class GroupSoftwareDelete(OrganizationPermission, generic.DeleteView):
model = ConfigGroupSoftware
permission_required = [
'config_management.delete_configgroupsoftware',
]
template_name = 'form.html.j2'
def get_success_url(self, **kwargs):
return reverse('Config Management:_group_view', args=(self.kwargs['group_id'],))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['content_title'] = 'Delete '
return context

89
app/core/http/browser.py Normal file
View File

@ -0,0 +1,89 @@
import requests
class Browser:
response: requests.Response = None
@property
def status() -> int:
""" HTTP Status Code
Returns:
int: Return the HTTP status code from the last request
"""
return self.response.status_code
def get(
self,
url: str,
headers: dict = {},
ssl_verify: bool = True
) -> requests.Response:
""" Perform a HTTP/GET request
Args:
url (str): URL to fetch.
headers (dict, optional): Request Headers. Defaults to {}.
ssl_verify (bool, optional): Verify the SSL Certificate. Defaults to True.
Returns:
requests.Response: The requests response object
"""
headers.update({
"Accept": "application/json",
"Authorization": "Bearer xx" # AWX auth
})
response = requests.get(
headers = headers,
timeout = 3,
url = url,
verify = ssl_verify,
)
if response.status_code == 200:
self.response = response
return self.response
def post(
self,
url: str,
headers: dict = {},
data: dict = None,
ssl_verify: bool = True
) -> requests.Response:
""" Perform an HTTP/POST request
Args:
url (str): _description_
headers (dict, optional): Request Headers. Defaults to {}.
data (dict, optional): _description_. Defaults to None.
ssl_verify (bool, optional): Verify the SSL Certificate. Defaults to True.
Returns:
requests.Response: _description_
"""
response = request.post(
headers={
"Content-Type": "application/json"
},
timeout = 3,
url = url,
data = data,
verify = ssl_verify,
)
if response.status_code == 200:
self.response = response
return self.response

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-06-11 20:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0008_alter_manufacturer_organization_and_more'),
]
operations = [
migrations.AddField(
model_name='manufacturer',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='notes',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
]

View File

View File

@ -11,18 +11,17 @@ class SaveHistory(models.Model):
class Meta:
abstract = True
@property
def fields(self):
return [ f.name for f in self._meta.fields + self._meta.many_to_many ]
def save_history(self, before: dict, after: dict):
""" Save a Models Changes
def save(self, *args, **kwargs):
""" OverRides save for keeping model history.
Not a Full-Override as this is just to add to existing.
Before to fetch from DB to ensure the changed value is the actual changed value and the after
is the data that was saved to the DB.
Args:
before (dict): model before saving (model.objects.get().__dict__)
after (dict): model after saving and refetched from DB (model.objects.get().__dict__)
"""
remove_keys = [
@ -30,12 +29,6 @@ class SaveHistory(models.Model):
'created',
'modified'
]
before = {}
try:
before = self.__class__.objects.get(pk=self.pk).__dict__.copy()
except Exception:
pass
clean = {}
for entry in before:
@ -58,11 +51,6 @@ class SaveHistory(models.Model):
before_json = json.dumps(clean)
# Process the save
super().save(*args, **kwargs)
after = self.__dict__.copy()
clean = {}
for entry in after:
@ -89,59 +77,36 @@ class SaveHistory(models.Model):
clean[entry] = value
after = json.dumps(clean)
after_json = json.dumps(clean)
item_parent_pk = None
item_parent_class = None
if self._meta.model_name == 'deviceoperatingsystem':
item_parent_pk = self.device.pk
item_parent_class = self.device._meta.model_name
if hasattr(self, 'parent_object'):
if self._meta.model_name == 'devicesoftware':
if self.parent_object:
item_parent_pk = self.device.pk
item_parent_class = self.device._meta.model_name
if self._meta.model_name == 'operatingsystemversion':
item_parent_pk = self.operating_system_id
item_parent_class = self.operating_system._meta.model_name
item_parent_pk = self.parent_object.pk
item_parent_class = self.parent_object._meta.model_name
if self._meta.model_name == 'softwareversion':
item_parent_pk = self.software.pk
item_parent_class = self.software._meta.model_name
if self._meta.model_name == 'team':
item_parent_pk = self.organization.pk
item_parent_class = self.organization._meta.model_name
if self._meta.model_name == 'teamusers':
item_parent_pk = self.team.pk
item_parent_class = self.team._meta.model_name
if self._meta.model_name == 'configgrouphosts':
item_parent_pk = self.group.id
item_parent_class = self.group._meta.model_name
item_pk = self.pk
if not before:
action = History.Actions.ADD
elif before != after:
elif before_json != after_json and self.pk:
action = History.Actions.UPDATE
elif not after:
elif self.pk is None:
action = History.Actions.DELETE
item_pk = before['id']
after_json = None
current_user = None
if get_request() is not None:
@ -152,16 +117,105 @@ class SaveHistory(models.Model):
current_user = None
if before != after and after != '{}':
# if before != after_json and after_json != '{}':
if before_json != after_json:
entry = History.objects.create(
before = before_json,
after = after,
after = after_json,
user = current_user,
action = action,
item_pk = self.pk,
item_pk = item_pk,
item_class = self._meta.model_name,
item_parent_pk = item_parent_pk,
item_parent_class = item_parent_class,
)
entry.save()
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
""" OverRides save for keeping model history.
Not a Full-Override as this is just to add to existing.
Before to fetch from DB to ensure the changed value is the actual changed value and the after
is the data that was saved to the DB.
"""
before = {}
try:
before = self.__class__.objects.get(pk=self.pk).__dict__.copy()
except Exception:
pass
# Process the save
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
after = self.__dict__.copy()
self.save_history(before, after)
def delete_history(self, item_pk, item_class):
""" Delete the objects history
When an object is no longer in the database, delete the objects history and
that of the child objects. Only caveat is that if the history has a parent_pk
the object history is not to be deleted.
Args:
item_pk (int): Primary key of the object to be deleted
item_class (str): Object class of the object to be deleted
"""
object_history = History.objects.filter(
item_pk = item_pk,
item_class = item_class,
item_parent_pk = None,
)
if object_history.exists():
object_history.delete()
child_object_history = History.objects.filter(
item_parent_pk = item_pk,
item_parent_class = item_class,
)
if child_object_history.exists():
child_object_history.delete()
def delete(self, using=None, keep_parents=False):
""" OverRides delete for keeping model history and on parent object ONLY!.
Not a Full-Override as this is just to add to existing.
"""
before = {}
item_pk = self.pk
item_class = self._meta.model_name
try:
before = self.__class__.objects.get(pk=self.pk).__dict__.copy()
except Exception:
pass
# Process the delete
super().delete(using=using, keep_parents=keep_parents)
after = self.__dict__.copy()
if hasattr(self, 'parent_object'):
self.save_history(before, after)
else:
self.delete_history(item_pk, item_class)

View File

@ -1,7 +1,7 @@
{% extends 'base.html.j2' %}
{% load json %}
{% block body %}
{% block content %}
<script>

View File

@ -10,4 +10,8 @@ register = template.Library()
@stringfilter
def json_pretty(value):
if value == 'None':
return str('{}')
return json.dumps(json.loads(value), indent=4, sort_keys=True)

View File

View File

View File

@ -0,0 +1,110 @@
import pytest
import unittest
from core.models.history import History
class HistoryEntry:
# field type testing to be done as part of model testing
def test_history_entry_item_add_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_create.__dict__
assert history['action'] == int(History.Actions.ADD[0])
# assert type(history['action']) is int
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_add_field_after(self):
""" Ensure after field contains correct value """
history = self.history_create.__dict__
assert history['after'] == str('{}')
# assert type(history['after']) is str
def test_history_entry_item_add_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_create.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_history_entry_item_add_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_create.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_history_entry_item_add_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_create.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
################################## Change ##################################
# field type testing to be done as part of model testing
def test_history_entry_item_change_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_change.__dict__
assert history['action'] == int(History.Actions.UPDATE[0])
# assert type(history['action']) is int
def test_history_entry_item_change_field_after(self):
""" Ensure after field contains correct value """
history = self.history_change.__dict__
assert history['after'] == str(self.field_after_expected_value)
# assert type(history['after']) is str
@pytest.mark.skip(reason="to be written")
def test_history_entry_item_change_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_change.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_history_entry_item_change_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_change.__dict__
assert history['item_pk'] == self.item_create.pk
# assert type(history['item_pk']) is int
def test_history_entry_item_change_field_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_change.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str

View File

@ -0,0 +1,82 @@
import pytest
import unittest
from core.models.history import History
class HistoryEntryChildItem:
def test_history_entry_item_delete_children_entries_not_exist(self):
""" When an item is deleted, it's children history entries must be removed """
assert self.history_delete_children.exists() is False
def test_history_entry_item_delete_field_action(self):
""" Ensure action is "add" for item creation """
history = self.history_delete.__dict__
assert history['action'] == int(History.Actions.DELETE[0])
# assert type(history['action']) is int
def test_history_entry_item_delete_field_after(self):
""" Ensure after field contains correct value """
history = self.history_delete.__dict__
assert history['after'] == None
# assert type(history['after']) is str
@pytest.mark.skip(reason="figure out best way to test")
def test_history_entry_item_delete_field_before(self):
""" Ensure before field is an empty JSON string for create """
history = self.history_delete.__dict__
assert history['before'] == str('{}')
# assert type(history['before']) is str
def test_history_entry_item_delete_field_item_pk(self):
""" Ensure history entry field item_pk is the created items pk """
history = self.history_delete.__dict__
assert history['item_pk'] == self.deleted_pk
# assert type(history['item_pk']) is int
def test_history_entry_item_delete_item_class(self):
""" Ensure history entry field item_class is the model name """
history = self.history_delete.__dict__
assert history['item_class'] == self.model._meta.model_name
# assert type(history['item_class']) is str
def test_history_entry_item_delete_field_parent_pk(self):
""" Ensure history entry field item_pk is the created parents pk """
history = self.history_delete.__dict__
assert history['item_parent_pk'] == self.item_parent.pk
# assert type(history['item_pk']) is int
def test_history_entry_item_delete_field_parent_class(self):
""" Ensure history entry field parent_class is the model name """
history = self.history_delete.__dict__
assert history['item_parent_class'] == self.item_parent._meta.model_name
# assert type(history['item_class']) is str

View File

@ -0,0 +1,21 @@
import pytest
import unittest
from core.models.history import History
class HistoryEntryParentItem:
def test_history_entry_delete(self):
""" When an item is deleted, it's history entries must be removed """
assert self.history_delete.exists() is False
def test_history_entry_children_delete(self):
""" When an item is deleted, it's history entries must be removed """
assert self.history_delete_children.exists() is False

View File

@ -0,0 +1,115 @@
import pytest
import unittest
from django.contrib.auth.models import User
from django.shortcuts import reverse
from django.test import TestCase, Client
from core.models.history import History
from itam.models.device import Device
class HistoryPermissions:
"""Test cases for accessing History """
item: object
"""Created Model
Create a new item.
"""
model = History
""" The history Model """
namespace: str = ''
""" URL namespace for the history view"""
name_view: str = '_history'
""" URL view name for history """
no_permissions_user: User
"""A User with no permissions to access the item
Create in `setUpTestData`
"""
different_organization_user: User
"""A User with the correct permissions to access the item
This user must be in a different organization than the item
Create in `setUpTestData`
"""
view_user: User
"""A User with the correct permissions to access the item
This user must be in the same organization as the item
Create in `setUpTestData`
"""
def test_view_history_user_anon_denied(self):
""" Check correct permission for view
Attempt to view as anon user
"""
client = Client()
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.item._meta.model_name, 'model_pk': self.item.id})
response = client.get(url)
assert response.status_code == 302 and response.url.startswith('/account/login')
def test_view_history_no_permission_denied(self):
""" Check correct permission for view
Attempt to view with user missing permission
"""
client = Client()
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.item._meta.model_name, 'model_pk': self.item.id})
client.force_login(self.no_permissions_user)
response = client.get(url)
assert response.status_code == 403
def test_view_history_different_organizaiton_denied(self):
""" Check correct permission for view
Attempt to view with user from different organization
"""
client = Client()
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.item._meta.model_name, 'model_pk': self.item.id})
client.force_login(self.different_organization_user)
response = client.get(url)
assert response.status_code == 403
def test_view_history_has_permission(self):
""" Check correct permission for view
Attempt to view as user with view permission
"""
client = Client()
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.item._meta.model_name, 'model_pk': self.item.id})
client.force_login(self.view_user)
response = client.get(url)
assert response.status_code == 200

View File

@ -0,0 +1,75 @@
import pytest
import unittest
import requests
from django.test import TestCase, Client
from access.models import Organization
from core.models.history import History
from core.models.manufacturer import Manufacturer
from core.tests.abstract.history_entry import HistoryEntry
from core.tests.abstract.history_entry_parent_model import HistoryEntryParentItem
from itam.models.device import Device
class ManufacturerHistory(TestCase, HistoryEntry, HistoryEntryParentItem):
model = Manufacturer
@classmethod
def setUpTestData(self):
""" Setup Test """
organization = Organization.objects.create(name='test_org')
self.organization = organization
self.item_create = self.model.objects.create(
name = 'test_item_' + self.model._meta.model_name,
organization = self.organization
)
self.history_create = History.objects.get(
action = History.Actions.ADD[0],
item_pk = self.item_create.pk,
item_class = self.model._meta.model_name,
)
self.item_change = self.item_create
self.item_change.name = 'test_item_' + self.model._meta.model_name + '_changed'
self.item_change.save()
self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}'
self.history_change = History.objects.get(
action = History.Actions.UPDATE[0],
item_pk = self.item_change.pk,
item_class = self.model._meta.model_name,
)
self.item_delete = self.model.objects.create(
name = 'test_item_delete_' + self.model._meta.model_name,
organization = self.organization
)
self.deleted_pk = self.item_delete.pk
self.item_delete.delete()
self.history_delete = History.objects.filter(
item_pk = self.deleted_pk,
item_class = self.model._meta.model_name,
)
self.history_delete_children = History.objects.filter(
item_parent_pk = self.deleted_pk,
item_parent_class = self.model._meta.model_name,
)

View File

@ -0,0 +1,94 @@
import pytest
import unittest
import requests
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase, Client
from access.models import Organization, Team, TeamUsers, Permission
from core.models.manufacturer import Manufacturer
from core.tests.abstract.history_permissions import HistoryPermissions
class ManufacturerHistoryPermissions(TestCase, HistoryPermissions):
item_model = Manufacturer
@classmethod
def setUpTestData(self):
"""Setup Test
1. Create an organization for user and item
2. create an organization that is different to item
3. Create a device
4. Add history device history entry as item
5. create a user
6. create user in different organization (with the required permission)
"""
organization = Organization.objects.create(name='test_org')
self.organization = organization
different_organization = Organization.objects.create(name='test_different_organization')
self.item = self.item_model.objects.create(
organization=organization,
name = 'deviceone'
)
self.history_model_name = self.item._meta.model_name
self.history = self.model.objects.get(
item_pk = self.item.id,
item_class = self.item._meta.model_name,
action = self.model.Actions.ADD,
)
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])
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.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,
])
TeamUsers.objects.create(
team = different_organization_team,
user = self.different_organization_user
)

View File

@ -0,0 +1,187 @@
# from django.conf import settings
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 app.tests.abstract.model_permissions import ModelPermissions
from core.models.manufacturer import Manufacturer
class ManufacturerPermissions(TestCase, ModelPermissions):
model = Manufacturer
app_namespace = 'Settings'
url_name_view = '_manufacturer_view'
url_name_add = '_manufacturer_add'
url_name_change = '_manufacturer_view'
url_name_delete = '_manufacturer_delete'
url_delete_response = reverse('Settings:_manufacturers')
@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 manufacturer
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 = 'manufacturerone'
)
self.url_view_kwargs = {'pk': self.item.id}
# self.url_add_kwargs = {'pk': self.item.id}
self.add_data = {'manufacturer': 'manufacturer', 'organization': self.organization.id}
self.url_change_kwargs = {'pk': self.item.id}
self.change_data = {'manufacturer': 'manufacturer', 'organization': self.organization.id}
self.url_delete_kwargs = {'pk': self.item.id}
self.delete_data = {'manufacturer': 'manufacturer', '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
)

View File

@ -35,3 +35,48 @@ class History(TestCase):
fields required `parent_item_pk` and `parent_item_class
"""
pass
@pytest.mark.skip(reason="to be written")
def test_history_save_calls_save_history():
""" During model save, self.save_history is called
This method saves the history to the database
"""
pass
@pytest.mark.skip(reason="to be written")
def test_history_delete_calls_save_history():
""" During model delete, self.save_history is called
This method saves the delete history to the database for parent objects
"""
pass
@pytest.mark.skip(reason="to be written")
def test_history_delete_calls_delete_history():
""" During model delete, self.delete_history is called
This method deletes the item and child-item history from the database
"""
pass
@pytest.mark.skip(reason="to be written")
def test_function_save_attributes():
""" Ensure save Attributes function match django default
the save method is overridden. the function attributes must match default django method
"""
pass
@pytest.mark.skip(reason="to be written")
def test_function_delete_attributes():
""" Ensure delete Attributes function match django default
the delete method is overridden. the function attributes must match default django method
"""
pass

View File

@ -13,6 +13,7 @@ from access.models import Organization, Team, TeamUsers, Permission
from itam.models.device import Device
@pytest.mark.skip(reason="this test needs to move to models tests that recieve notes")
class NotesPermissions(TestCase):
model = Device

View File

@ -18,17 +18,21 @@ class DeviceForm(forms.ModelForm):
'serial_number',
'uuid',
'device_type',
'organization'
'organization',
'model_notes',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['_lastinventory'] = forms.DateTimeField(
self.fields['lastinventory'] = forms.DateTimeField(
label="Last Inventory Date",
input_formats=settings.DATETIME_FORMAT,
initial=kwargs['instance'].inventorydate,
disabled=True,
required=False,
)
# for key in self.fields.keys():
# self.fields[key].widget.attrs['disabled'] = True

View File

@ -16,6 +16,7 @@ class Update(forms.ModelForm):
'id',
'organization',
'is_global',
'model_notes',
]

View File

@ -16,6 +16,7 @@ class Update(forms.ModelForm):
'organization',
'is_global',
'category',
'model_notes',
]
def __init__(self, *args, **kwargs):

View File

@ -0,0 +1,63 @@
# Generated by Django 5.0.6 on 2024-06-11 20:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('itam', '0013_alter_device_organization_and_more'),
]
operations = [
migrations.AddField(
model_name='device',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='devicemodel',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='deviceoperatingsystem',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='devicesoftware',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='devicetype',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='operatingsystem',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='operatingsystemversion',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='software',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='softwarecategory',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='softwareversion',
name='model_notes',
field=models.TextField(blank=True, default=None, null=True),
),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 5.0.6 on 2024-06-17 08:56
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('itam', '0014_device_model_notes_devicemodel_model_notes_and_more'),
]
operations = [
migrations.AlterField(
model_name='device',
name='device_model',
field=models.ForeignKey(blank=True, default=None, help_text='Model of the device.', null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.devicemodel'),
),
migrations.AlterField(
model_name='device',
name='device_type',
field=models.ForeignKey(blank=True, default=None, help_text='Type of device.', null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.devicetype'),
),
migrations.AlterField(
model_name='device',
name='serial_number',
field=models.CharField(blank=True, default=None, help_text='Serial number of the device.', max_length=50, null=True, unique=True, verbose_name='Serial Number'),
),
migrations.AlterField(
model_name='device',
name='uuid',
field=models.CharField(blank=True, default=None, help_text='System GUID/UUID.', max_length=50, null=True, unique=True, verbose_name='UUID'),
),
]

View File

View File

@ -1,12 +1,15 @@
import json
from datetime import timedelta
from django.db import models
from access.fields import *
from access.models import TenancyObject
from core.mixin.history_save import SaveHistory
from app.helpers.merge_software import merge_software
from core.mixin.history_save import SaveHistory
from itam.models.device_common import DeviceCommonFields, DeviceCommonFieldsName
from itam.models.device_models import DeviceModel
@ -43,6 +46,7 @@ class Device(DeviceCommonFieldsName, SaveHistory):
null = True,
blank = True,
unique = True,
help_text = 'Serial number of the device.',
)
@ -53,7 +57,7 @@ class Device(DeviceCommonFieldsName, SaveHistory):
null = True,
blank = True,
unique = True,
help_text = 'System GUID/UUID.',
)
device_model = models.ForeignKey(
@ -61,7 +65,8 @@ class Device(DeviceCommonFieldsName, SaveHistory):
on_delete=models.CASCADE,
default = None,
null = True,
blank= True
blank= True,
help_text = 'Model of the device.',
)
device_type = models.ForeignKey(
@ -69,7 +74,8 @@ class Device(DeviceCommonFieldsName, SaveHistory):
on_delete=models.CASCADE,
default = None,
null = True,
blank= True
blank= True,
help_text = 'Type of device.',
)
@ -77,7 +83,7 @@ class Device(DeviceCommonFieldsName, SaveHistory):
inventorydate = models.DateTimeField(
verbose_name = 'Last Inventory Date',
null = True,
blank = True
blank = True,
)
@ -85,6 +91,40 @@ class Device(DeviceCommonFieldsName, SaveHistory):
return self.name
@property
def status(self) -> str:
""" Fetch Device status
Returns:
str: Current status of the item
"""
if self.inventorydate:
check_date = self.inventorydate
else:
check_date = now() + timedelta(days=99)
one = (now() - check_date).days
if (now() - check_date).days >= 0 and (now() - check_date).days <= 1:
return 'OK'
elif (now() - check_date).days >= 2 and (now() - check_date).days < 3:
return 'WARN'
elif (now() - check_date).days >= 3:
return 'BAD'
else:
return 'UNK'
def get_configuration(self, id):
@ -94,6 +134,9 @@ class Device(DeviceCommonFieldsName, SaveHistory):
"software": []
}
host_software = []
group_software = []
for software in softwares:
if software.action:
@ -115,7 +158,7 @@ class Device(DeviceCommonFieldsName, SaveHistory):
if software.version:
software_action['version'] = software.version.name
config['software'] = config['software'] + [ software_action ]
host_software += [ software_action ]
config: dict = config
@ -131,7 +174,15 @@ class Device(DeviceCommonFieldsName, SaveHistory):
if rendered_config:
config.update(json.loads(group.group.render_config()))
config.update(json.loads(rendered_config))
rendered_config: dict = json.loads(rendered_config)
if 'software' in rendered_config.keys():
group_software = group_software + rendered_config['software']
config['software'] = merge_software(group_software, host_software)
return config
@ -200,6 +251,13 @@ class DeviceSoftware(DeviceCommonFields, SaveHistory):
)
@property
def parent_object(self):
""" Fetch the parent object """
return self.device
class DeviceOperatingSystem(DeviceCommonFields, SaveHistory):
@ -235,3 +293,10 @@ class DeviceOperatingSystem(DeviceCommonFields, SaveHistory):
blank = True,
default = None,
)
@property
def parent_object(self):
""" Fetch the parent object """
return self.device

View File

@ -69,6 +69,14 @@ class OperatingSystemVersion(OperatingSystemCommonFields, SaveHistory):
unique = False,
)
@property
def parent_object(self):
""" Fetch the parent object """
return self.operating_system
def __str__(self):
return self.operating_system.name + ' ' + self.name

View File

@ -102,6 +102,14 @@ class SoftwareVersion(SoftwareCommonFields, SaveHistory):
unique = False,
)
@property
def parent_object(self):
""" Fetch the parent object """
return self.software
def __str__(self):
return self.name

View File

@ -1,8 +1,10 @@
{% extends 'base.html.j2' %}
{% load markdown %}
{% block title %}{{ device.name }}{% endblock %}
{% block body %}
{% block content %}
<script>
@ -37,12 +39,43 @@
<path d="m313-480 155 156q11 11 11.5 27.5T468-268q-11 11-28 11t-28-11L228-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T468-692q11 11 11 28t-11 28L313-480Zm264 0 155 156q11 11 11.5 27.5T732-268q-11 11-28 11t-28-11L492-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T732-692q11 11 11 28t-11 28L577-480Z" />
</svg> Back to Devices</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'Details')">Details</button>
<button id="defaultOpen" class="tablinks" onclick="openCity(event, 'OperatingSystem')">Operating System</button>
<button class="tablinks" onclick="openCity(event, 'Software')">Software</button>
<button class="tablinks" onclick="openCity(event, 'Notes')">Notes</button>
<button class="tablinks" onclick="openCity(event, 'ConfigManagement')">Config Management</button>
<!-- <button class="tablinks" onclick="openCity(event, 'Installations')">Installations</button> -->
</div>
<style>
.detail-view-field {
display:unset;
height: 30px;
line-height: 30px;
padding: 0px 20px 40px 20px;
}
.detail-view-field label {
display: inline-block;
font-weight: bold;
width: 200px;
margin: 10px;
/*padding: 10px;*/
height: 30px;
line-height: 30px;
}
.detail-view-field span {
display: inline-block;
width: 340px;
margin: 10px;
/*padding: 10px;*/
border-bottom: 1px solid #ccc;
height: 30px;
line-height: 30px;
}
</style>
<form action="" method="post">
{% csrf_token %}
@ -52,9 +85,68 @@
Details
<span style="font-weight: normal; float: right;">{% include 'icons/issue_link.html.j2' with issue=6 %}</span>
</h3>
{{ form.as_p }}
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
<input name="{{form.prefix}}" type="submit" value="Submit">
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">
<div style="display: inline; width: 40%; margin: 30px;">
<div class="detail-view-field">
<label>{{ form.name.label }}</label>
<span>{{ form.name.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.device_model.label }}</label>
<span>{{ form.device_model.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.serial_number.label }}</label>
<span>{{ form.serial_number.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.uuid.label }}</label>
<span>{{ form.uuid.value }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.device_type.label }}</label>
<span>{{ device.device_type }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.organization.label }}</label>
<span>{{ device.organization }}</span>
</div>
<div class="detail-view-field">
<label>{{ form.lastinventory.label }}</label>
<span>{{ form.lastinventory.value }}</span>
</div>
</div>
<div style="display: inline; width: 40%; margin: 30px; text-align: left;">
<div>
<label style="font-weight: bold; width: 100%; border-bottom: 1px solid #ccc; display: block; text-align: inherit;">{{ form.model_notes.label }}</label>
<div style="display: inline-block; text-align: left;">{{ form.model_notes.value | markdown | safe }}</div>
{% include 'icons/issue_link.html.j2' with issue=13 %}<br>
</div>
</div>
</div>
<input type="button" value="Edit" onclick="window.location='{% url 'ITAM:_device_change' device.id %}';">
<div style="display: block; width: 100%;">
<h3>Operating System</h3>
<br>
{{ operating_system.as_p }}
<input type="submit" name="{{operating_system.prefix}}" value="Submit" />
</div>
<script>
// Get the element with id="defaultOpen" and click on it
document.getElementById("defaultOpen").click();
@ -62,11 +154,6 @@
</div>
<div id="OperatingSystem" class="tabcontent">
<h3>Operating System</h3>
{{ operating_system.as_p }}
<input type="submit" name="{{operating_system.prefix}}" value="Submit" />
</div>
<div id="Software" class="tabcontent">
<h3>Software</h3>
@ -126,6 +213,24 @@
<td colspan="5">Nothing Found</td>
{% endif %}
</table>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">&laquo; first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
{% endif %}
</span>
</div>
</div>

View File

@ -3,11 +3,78 @@
{% block title %}Devices{% endblock %}
{% block content_header_icon %}{% endblock %}
{% block body %}
{% block content %}
<style>
a.status_icon {
display: inline-block;
height: 30px;
line-height: 30px;
/*background-color: aqua;*/
}
span.status_icon {
/* display: inline-block;*/
height: 30px;
line-height: 30px;
/*background-color: aqua;*/
width: auto;
align-items: center;
/*position: relative;*/
vertical-align: middle; padding: auto; margin: 0px
}
.status_icon_ok svg {
fill: #319c3a;
width: 22px;
height: 22px;
vertical-align: middle;
margin: 0px;
padding: 0px;
border: none;
border-radius: 11px;
/*background-color: #c7e2ca;*/
}
.status_icon_warn svg {
fill: #cf9925;
width: 22px;
height: 22px;
vertical-align: middle;
margin: 0px;
padding: 0px;
border: none;
border-radius: 11px;
/*background-color: #ffefc5;*/
}
.status_icon_bad svg {
fill: #cf3025;
width: 22px;
height: 22px;
vertical-align: middle;
margin: 0px;
padding: 0px;
border: none;
border-radius: 11px;
/*background-color: #ffefc5;*/
}
.status_icon_ukn svg {
fill: #999;
width: 22px;
height: 22px;
border: none;
border-radius: 11px;
/*background-color: #e9e9e9;*/
}
</style>
<input type="button" value="New Device" onclick="window.location='{% url 'ITAM:_device_add' %}';">
<table class="data">
<tr>
<th style="width: 50px;">&nbsp;</th>
<th>Name</th>
<th>Device Type</th>
<th>Manufacturer</th>
@ -17,7 +84,29 @@
</tr>
{% for device in devices %}
<tr>
<td><a href="{% url 'ITAM:_device_view' pk=device.id %}">{{ device.name }}</a></td>
<td>
<span class="status_icon status_icon_ok">
{% if device.status == 'OK' %}
<span class="status_icon status_icon_ok">
{% include 'icons/status_ok.svg' %}
</span>
{% elif device.status == 'WARN' %}
<span class="status_icon status_icon_warn">
{% include 'icons/status_ok.svg' %}
</span>
{% elif device.status == 'BAD' %}
<span class="status_icon status_icon_bad">
{% include 'icons/status_bad.svg' %}
</span>
{% else %}
<span class="status_icon status_icon_ukn">
{% include 'icons/status_unknown.svg' %}
</span>
{% endif %}
</td>
<td>
<a href="{% url 'ITAM:_device_view' pk=device.id %}">{{ device.name }}</a>
</td>
<td>
{% if device.device_type %}
{{ device.device_type }}

Some files were not shown because too many files have changed in this diff Show More