Merge pull request #658 from nofusscomputing/development
This commit is contained in:
@ -5,7 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||||||
from rest_framework import exceptions
|
from rest_framework import exceptions
|
||||||
from rest_framework.permissions import DjangoObjectPermissions
|
from rest_framework.permissions import DjangoObjectPermissions
|
||||||
|
|
||||||
from access.models.tenancy import TenancyObject
|
from access.models.tenancy import Organization, TenancyObject
|
||||||
|
|
||||||
from core import exceptions as centurion_exceptions
|
from core import exceptions as centurion_exceptions
|
||||||
|
|
||||||
@ -116,15 +116,27 @@ class OrganizationPermissionMixin(
|
|||||||
try:
|
try:
|
||||||
|
|
||||||
if (
|
if (
|
||||||
view.model.__name__ == 'UserSettings'
|
(
|
||||||
and request._user.id == int(view.kwargs.get('pk', 0))
|
view.model.__name__ == 'UserSettings'
|
||||||
|
and request._user.id == int(view.kwargs.get('pk', 0))
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
view.model.__name__ == 'AuthToken'
|
||||||
|
and request._user.id == int(view.kwargs.get('model_id', 0))
|
||||||
|
)
|
||||||
):
|
):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
elif (
|
elif (
|
||||||
view.model.__name__ == 'UserSettings'
|
(
|
||||||
and request._user.id != int(view.kwargs.get('pk', 0))
|
view.model.__name__ == 'UserSettings'
|
||||||
|
and request._user.id != int(view.kwargs.get('pk', 0))
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
view.model.__name__ == 'AuthToken'
|
||||||
|
and request._user.id != int(view.kwargs.get('model_id', 0))
|
||||||
|
)
|
||||||
):
|
):
|
||||||
|
|
||||||
|
|
||||||
@ -271,8 +283,14 @@ class OrganizationPermissionMixin(
|
|||||||
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
view.model.__name__ == 'UserSettings'
|
(
|
||||||
and request._user.id == int(view.kwargs.get('pk', 0))
|
view.model.__name__ == 'UserSettings'
|
||||||
|
and request._user.id == int(view.kwargs.get('pk', 0))
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
view.model.__name__ == 'AuthToken'
|
||||||
|
and request._user.id == int(view.kwargs.get('model_id', 0))
|
||||||
|
)
|
||||||
):
|
):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -32,7 +32,7 @@ class AuthTokenForm(CommonModelForm):
|
|||||||
|
|
||||||
if self.prefix + '-gen_token' not in self.data:
|
if self.prefix + '-gen_token' not in self.data:
|
||||||
|
|
||||||
generated_token = self.instance.generate()
|
generated_token = self.instance.generate
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
|
17
app/api/migrations/0003_alter_authtoken_options.py
Normal file
17
app/api/migrations/0003_alter_authtoken_options.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-02-28 15:12
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('api', '0002_alter_authtoken_expires_alter_authtoken_id_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='authtoken',
|
||||||
|
options={'ordering': ['expires'], 'verbose_name': 'Auth Token', 'verbose_name_plural': 'Auth Tokens'},
|
||||||
|
),
|
||||||
|
]
|
@ -3,20 +3,34 @@ import random
|
|||||||
import string
|
import string
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Field
|
|
||||||
from django.forms import ValidationError
|
from django.forms import ValidationError
|
||||||
|
|
||||||
from access.fields import *
|
from rest_framework.reverse import reverse
|
||||||
from access.models.tenancy import TenancyObject
|
|
||||||
|
from access.fields import (
|
||||||
|
AutoCreatedField,
|
||||||
|
AutoLastModifiedField
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AuthToken(models.Model):
|
class AuthToken(models.Model):
|
||||||
|
|
||||||
|
|
||||||
def validate_note_no_token(self, note, token):
|
class Meta:
|
||||||
|
|
||||||
|
ordering = [
|
||||||
|
'expires'
|
||||||
|
]
|
||||||
|
|
||||||
|
verbose_name = 'Auth Token'
|
||||||
|
|
||||||
|
verbose_name_plural = 'Auth Tokens'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def validate_note_no_token(self, note, token, raise_exception = True) -> bool:
|
||||||
""" Ensure plaintext token cant be saved to notes field.
|
""" Ensure plaintext token cant be saved to notes field.
|
||||||
|
|
||||||
called from app.settings.views.user_settings.TokenAdd.form_valid()
|
called from app.settings.views.user_settings.TokenAdd.form_valid()
|
||||||
@ -41,10 +55,12 @@ class AuthToken(models.Model):
|
|||||||
|
|
||||||
validation = False
|
validation = False
|
||||||
|
|
||||||
if not validation:
|
if not validation and raise_exception:
|
||||||
|
|
||||||
raise ValidationError('Token can not be placed in the notes field.')
|
raise ValidationError('Token can not be placed in the notes field.')
|
||||||
|
|
||||||
|
return validation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
id = models.AutoField(
|
id = models.AutoField(
|
||||||
@ -95,6 +111,7 @@ class AuthToken(models.Model):
|
|||||||
modified = AutoLastModifiedField()
|
modified = AutoLastModifiedField()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
def generate(self) -> str:
|
def generate(self) -> str:
|
||||||
|
|
||||||
return str(hashlib.sha256(str(self.randomword()).encode('utf-8')).hexdigest())
|
return str(hashlib.sha256(str(self.randomword()).encode('utf-8')).hexdigest())
|
||||||
@ -115,3 +132,33 @@ class AuthToken(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
||||||
return self.token
|
return self.token
|
||||||
|
|
||||||
|
|
||||||
|
table_fields: list = [
|
||||||
|
'note',
|
||||||
|
'created',
|
||||||
|
'expires',
|
||||||
|
'-action_delete-',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_url( self, request = None ) -> str:
|
||||||
|
|
||||||
|
if request:
|
||||||
|
|
||||||
|
return reverse(f"v2:_api_v2_user_settings_token-detail", request=request, kwargs = self.get_url_kwargs() )
|
||||||
|
|
||||||
|
return reverse(f"v2:_api_v2_user_settings_token-detail", kwargs = self.get_url_kwargs() )
|
||||||
|
|
||||||
|
|
||||||
|
def get_url_kwargs(self) -> dict:
|
||||||
|
"""Fetch the URL kwargs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: kwargs required for generating the URL with `reverse`
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
'model_id': self.user.id,
|
||||||
|
'pk': self.id
|
||||||
|
}
|
||||||
|
136
app/api/serializers/auth_token.py
Normal file
136
app/api/serializers/auth_token.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from access.serializers.organization import OrganizationBaseSerializer
|
||||||
|
|
||||||
|
from api.models.tokens import AuthToken
|
||||||
|
from api.serializers import common
|
||||||
|
|
||||||
|
from core import exceptions as centurion_exception
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AuthTokenBaseSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
display_name = serializers.SerializerMethodField('get_display_name')
|
||||||
|
|
||||||
|
def get_display_name(self, item) -> str:
|
||||||
|
|
||||||
|
return str( item )
|
||||||
|
|
||||||
|
url = serializers.HyperlinkedIdentityField(
|
||||||
|
view_name="v2:_api_v2_cluster-detail",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
model = AuthToken
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'display_name',
|
||||||
|
'url',
|
||||||
|
]
|
||||||
|
|
||||||
|
read_only_fields = [
|
||||||
|
'id',
|
||||||
|
'display_name',
|
||||||
|
'url',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AuthTokenModelSerializer(
|
||||||
|
common.CommonModelSerializer,
|
||||||
|
AuthTokenBaseSerializer
|
||||||
|
):
|
||||||
|
|
||||||
|
_urls = serializers.SerializerMethodField('get_url')
|
||||||
|
|
||||||
|
def get_url(self, item) -> dict:
|
||||||
|
|
||||||
|
get_url = super().get_url( item = item )
|
||||||
|
|
||||||
|
del get_url['history']
|
||||||
|
del get_url['knowledge_base']
|
||||||
|
del get_url['notes']
|
||||||
|
|
||||||
|
|
||||||
|
return get_url
|
||||||
|
|
||||||
|
|
||||||
|
expires = serializers.DateTimeField(
|
||||||
|
initial = (datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=90)).replace(microsecond=0).isoformat(),
|
||||||
|
)
|
||||||
|
|
||||||
|
token = serializers.CharField(initial=AuthToken().generate, write_only = True )
|
||||||
|
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
model = AuthToken
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'display_name',
|
||||||
|
'note',
|
||||||
|
'token',
|
||||||
|
'user',
|
||||||
|
'expires',
|
||||||
|
'created',
|
||||||
|
'modified',
|
||||||
|
'_urls',
|
||||||
|
]
|
||||||
|
|
||||||
|
read_only_fields = [
|
||||||
|
'id',
|
||||||
|
'display_name',
|
||||||
|
'user',
|
||||||
|
'created',
|
||||||
|
'modified',
|
||||||
|
'_urls',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
|
||||||
|
if self.context['request'].user.id != int(self.context['view'].kwargs['model_id']):
|
||||||
|
|
||||||
|
raise centurion_exception.PermissionDenied()
|
||||||
|
|
||||||
|
|
||||||
|
if not self.Meta.model().validate_note_no_token(attrs['note'], attrs['token']):
|
||||||
|
|
||||||
|
raise centurion_exception.ValidationError(
|
||||||
|
detail = {
|
||||||
|
"note": "No more than nine chars of token can be contained within the notes field"
|
||||||
|
},
|
||||||
|
code = 'note_no_contain_token'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if not re.fullmatch(r'[0-9|a-f]{64}', str(attrs['token']).lower()):
|
||||||
|
|
||||||
|
|
||||||
|
raise centurion_exception.ValidationError(
|
||||||
|
detail = {
|
||||||
|
"token": "Token appears to have been edited."
|
||||||
|
},
|
||||||
|
code = 'token_not_sha256'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
attrs['token'] = self.Meta.model().token_hash(attrs['token'])
|
||||||
|
|
||||||
|
attrs = super().validate(attrs)
|
||||||
|
|
||||||
|
attrs['user'] = self.context['request'].user
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AuthTokenViewSerializer(AuthTokenModelSerializer):
|
||||||
|
|
||||||
|
organization = OrganizationBaseSerializer( many = False, read_only = True )
|
@ -0,0 +1,162 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from rest_framework.exceptions import ValidationError, PermissionDenied
|
||||||
|
|
||||||
|
from access.models.organization import Organization
|
||||||
|
|
||||||
|
from api.serializers.auth_token import AuthToken, AuthTokenModelSerializer
|
||||||
|
|
||||||
|
from app.tests.abstract.mock_view import MockView, User
|
||||||
|
|
||||||
|
# from core.serializers.manufacturer import Manufacturer, ManufacturerModelSerializer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationAPI(
|
||||||
|
TestCase,
|
||||||
|
):
|
||||||
|
|
||||||
|
model = AuthToken
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(self):
|
||||||
|
"""Setup Test
|
||||||
|
|
||||||
|
1. Create an org
|
||||||
|
2. Create an item
|
||||||
|
"""
|
||||||
|
|
||||||
|
organization = Organization.objects.create(name='test_org')
|
||||||
|
|
||||||
|
self.organization = organization
|
||||||
|
|
||||||
|
self.user = User.objects.create_user(username="test_user_view", password="password")
|
||||||
|
|
||||||
|
self.valid_data = {
|
||||||
|
'note': 'a note',
|
||||||
|
'token': self.model().generate,
|
||||||
|
'user': self.user.id,
|
||||||
|
'expires': '2025-02-26T00:09Z'
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mock_view = MockView( user = self.user )
|
||||||
|
|
||||||
|
self.mock_view.kwargs = {
|
||||||
|
'model_id': self.user.id
|
||||||
|
}
|
||||||
|
|
||||||
|
self.item = self.model.objects.create(
|
||||||
|
note = 'object note',
|
||||||
|
token = self.model().generate,
|
||||||
|
user = self.user,
|
||||||
|
expires = '2025-02-26T00:07Z'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_serializer_validation_valid_data(self):
|
||||||
|
"""Serializer Validation Check
|
||||||
|
|
||||||
|
Ensure that if creating with valid data, the object is created
|
||||||
|
"""
|
||||||
|
|
||||||
|
serializer = AuthTokenModelSerializer(
|
||||||
|
context = {
|
||||||
|
'request': self.mock_view.request,
|
||||||
|
'view': self.mock_view,
|
||||||
|
},
|
||||||
|
data = self.valid_data
|
||||||
|
)
|
||||||
|
|
||||||
|
assert serializer.is_valid(raise_exception = True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_serializer_validation_valid_data_different_user(self):
|
||||||
|
"""Serializer Validation Check
|
||||||
|
|
||||||
|
Ensure that if adding the same manufacturer
|
||||||
|
it raises a validation error
|
||||||
|
"""
|
||||||
|
|
||||||
|
class MockUser:
|
||||||
|
|
||||||
|
id = 99
|
||||||
|
|
||||||
|
mock_view = MockView( user = self.user )
|
||||||
|
|
||||||
|
mock_view.request.user = MockUser()
|
||||||
|
|
||||||
|
mock_view.kwargs = {
|
||||||
|
'model_id': self.user.id
|
||||||
|
}
|
||||||
|
|
||||||
|
with pytest.raises(PermissionDenied) as err:
|
||||||
|
|
||||||
|
serializer = AuthTokenModelSerializer(
|
||||||
|
context = {
|
||||||
|
'request': mock_view.request,
|
||||||
|
'view': mock_view,
|
||||||
|
},
|
||||||
|
data = self.valid_data
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer.is_valid(raise_exception = True)
|
||||||
|
|
||||||
|
assert err.value.get_codes() == 'permission_denied'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_serializer_validation_valid_data_token_not_sha256_same_length(self):
|
||||||
|
"""Serializer Validation Check
|
||||||
|
|
||||||
|
Ensure that if adding the same manufacturer
|
||||||
|
it raises a validation error
|
||||||
|
"""
|
||||||
|
|
||||||
|
valid_data = self.valid_data.copy()
|
||||||
|
|
||||||
|
valid_data['token'] = str(valid_data['token'])[:-5] + 'qwert'
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError) as err:
|
||||||
|
|
||||||
|
serializer = AuthTokenModelSerializer(
|
||||||
|
context = {
|
||||||
|
'request': self.mock_view.request,
|
||||||
|
'view': self.mock_view,
|
||||||
|
},
|
||||||
|
data = valid_data
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer.is_valid(raise_exception = True)
|
||||||
|
|
||||||
|
assert err.value.get_codes()['token'][0] == 'token_not_sha256'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_serializer_validation_valid_data_token_not_sha256_wrong_length(self):
|
||||||
|
"""Serializer Validation Check
|
||||||
|
|
||||||
|
Ensure that if adding the same manufacturer
|
||||||
|
it raises a validation error
|
||||||
|
"""
|
||||||
|
|
||||||
|
valid_data = self.valid_data.copy()
|
||||||
|
|
||||||
|
valid_data['token'] = str(valid_data['token'])[:-5] + 'qwer'
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError) as err:
|
||||||
|
|
||||||
|
serializer = AuthTokenModelSerializer(
|
||||||
|
context = {
|
||||||
|
'request': self.mock_view.request,
|
||||||
|
'view': self.mock_view,
|
||||||
|
},
|
||||||
|
data = valid_data
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer.is_valid(raise_exception = True)
|
||||||
|
|
||||||
|
assert err.value.get_codes()['token'][0] == 'token_not_sha256'
|
@ -0,0 +1,382 @@
|
|||||||
|
from django.contrib.auth.models import Permission, User
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.shortcuts import reverse
|
||||||
|
from django.test import Client, TestCase
|
||||||
|
|
||||||
|
from access.models.organization import Organization
|
||||||
|
from access.models.team import Team
|
||||||
|
from access.models.team_user import TeamUsers
|
||||||
|
|
||||||
|
from api.models.tokens import AuthToken
|
||||||
|
from api.tests.abstract.api_permissions_viewset import (
|
||||||
|
APIPermissionAdd,
|
||||||
|
APIPermissionDelete,
|
||||||
|
APIPermissionView,
|
||||||
|
)
|
||||||
|
from api.tests.abstract.api_serializer_viewset import (
|
||||||
|
SerializerAdd,
|
||||||
|
SerializerDelete,
|
||||||
|
SerializerView,
|
||||||
|
)
|
||||||
|
from api.tests.abstract.test_metadata_functional import (
|
||||||
|
MetadataAttributesFunctionalEndpoint,
|
||||||
|
MetadataAttributesFunctionalBase,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ViewSetBase:
|
||||||
|
|
||||||
|
model = AuthToken
|
||||||
|
|
||||||
|
app_namespace = 'v2'
|
||||||
|
|
||||||
|
url_name = '_api_v2_user_settings_token'
|
||||||
|
|
||||||
|
change_data = {'device_model_is_global': True}
|
||||||
|
|
||||||
|
delete_data = {}
|
||||||
|
|
||||||
|
@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 team
|
||||||
|
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.different_organization = different_organization
|
||||||
|
|
||||||
|
|
||||||
|
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.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.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||||
|
teamuser = TeamUsers.objects.create(
|
||||||
|
team = view_team,
|
||||||
|
user = self.view_user
|
||||||
|
)
|
||||||
|
|
||||||
|
self.item = self.model.objects.create(
|
||||||
|
note = 'a note',
|
||||||
|
token = self.model().generate,
|
||||||
|
user = self.view_user,
|
||||||
|
expires = '2025-02-25T23:14Z'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.item_delete = self.model.objects.create(
|
||||||
|
note = 'a note',
|
||||||
|
token = self.model().generate,
|
||||||
|
user = self.delete_user,
|
||||||
|
expires = '2025-02-25T23:14Z'
|
||||||
|
)
|
||||||
|
|
||||||
|
# self.item.default_organization = self.organization
|
||||||
|
|
||||||
|
# self.item.save()
|
||||||
|
|
||||||
|
|
||||||
|
self.url_view_kwargs = {
|
||||||
|
'model_id': self.view_user.id,
|
||||||
|
'pk': self.item.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.url_kwargs = {
|
||||||
|
'model_id': self.view_user.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.add_data = {
|
||||||
|
'note': 'a note',
|
||||||
|
'token': self.model().generate,
|
||||||
|
'expires': '2025-02-26T23:14Z'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionsAPI(
|
||||||
|
ViewSetBase,
|
||||||
|
APIPermissionAdd,
|
||||||
|
APIPermissionDelete,
|
||||||
|
APIPermissionView,
|
||||||
|
TestCase,
|
||||||
|
):
|
||||||
|
|
||||||
|
|
||||||
|
def test_returned_data_from_user_and_global_organizations_only(self):
|
||||||
|
"""Check items returned
|
||||||
|
|
||||||
|
This test case is a over-ride of a test case with the same name.
|
||||||
|
This model is not a tenancy model making this test not-applicable.
|
||||||
|
|
||||||
|
Items returned from the query Must be from the users organization and
|
||||||
|
global ONLY!
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_has_permission(self):
|
||||||
|
""" Check correct permission for add
|
||||||
|
|
||||||
|
Attempt to add as user with permission
|
||||||
|
"""
|
||||||
|
|
||||||
|
url_kwargs = self.url_kwargs.copy()
|
||||||
|
|
||||||
|
url_kwargs['model_id'] = self.add_user.id
|
||||||
|
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
if self.url_kwargs:
|
||||||
|
|
||||||
|
url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = url_kwargs)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
url = reverse(self.app_namespace + ':' + self.url_name + '-list')
|
||||||
|
|
||||||
|
|
||||||
|
client.force_login(self.add_user)
|
||||||
|
response = client.post(url, data=self.add_data)
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_permission_view_denied(self):
|
||||||
|
""" Check correct permission for add
|
||||||
|
|
||||||
|
Attempt to add a user with view permission
|
||||||
|
"""
|
||||||
|
|
||||||
|
url_kwargs = self.url_kwargs.copy()
|
||||||
|
|
||||||
|
url_kwargs['model_id'] = self.add_user.id
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
if self.url_kwargs:
|
||||||
|
|
||||||
|
url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = url_kwargs)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
url = reverse(self.app_namespace + ':' + self.url_name + '-list')
|
||||||
|
|
||||||
|
|
||||||
|
client.force_login(self.view_user)
|
||||||
|
response = client.post(url, data=self.add_data)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_has_permission(self):
|
||||||
|
""" Check correct permission for delete
|
||||||
|
|
||||||
|
Delete item as user with delete permission
|
||||||
|
"""
|
||||||
|
|
||||||
|
url_view_kwargs = self.url_view_kwargs.copy()
|
||||||
|
|
||||||
|
url_view_kwargs['model_id'] = self.delete_user.id
|
||||||
|
url_view_kwargs['pk'] = self.item_delete.id
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=url_view_kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
client.force_login(self.delete_user)
|
||||||
|
response = client.delete(url, data=self.delete_data)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_permission_view_denied(self):
|
||||||
|
""" Check correct permission for delete
|
||||||
|
|
||||||
|
Attempt to delete as user with veiw permission only
|
||||||
|
"""
|
||||||
|
|
||||||
|
url_view_kwargs = self.url_view_kwargs.copy()
|
||||||
|
|
||||||
|
url_view_kwargs['model_id'] = self.delete_user.id
|
||||||
|
url_view_kwargs['pk'] = self.item_delete.id
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=url_view_kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
client.force_login(self.view_user)
|
||||||
|
response = client.delete(url, data=self.delete_data)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_returned_results_only_user_orgs(self):
|
||||||
|
"""Test not required
|
||||||
|
|
||||||
|
this test is not required as this model is not a tenancy model
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_view_no_permission_denied(self):
|
||||||
|
""" Check correct permission for view
|
||||||
|
|
||||||
|
This test case is a duplicate of a test case with the same name.
|
||||||
|
This test is not required for this model as there are no permissions
|
||||||
|
assosiated with accessing this model.
|
||||||
|
|
||||||
|
Attempt to view with user missing permission
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ViewSet(
|
||||||
|
ViewSetBase,
|
||||||
|
SerializerAdd,
|
||||||
|
SerializerDelete,
|
||||||
|
SerializerView,
|
||||||
|
TestCase,
|
||||||
|
):
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Metadata(
|
||||||
|
ViewSetBase,
|
||||||
|
MetadataAttributesFunctionalEndpoint,
|
||||||
|
MetadataAttributesFunctionalBase,
|
||||||
|
TestCase
|
||||||
|
):
|
||||||
|
|
||||||
|
viewset_type = 'detail'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(self):
|
||||||
|
|
||||||
|
super().setUpTestData()
|
||||||
|
|
||||||
|
self.url_kwargs = self.url_view_kwargs
|
@ -55,7 +55,7 @@ class APIAuthToken(TestCase):
|
|||||||
expires=expires
|
expires=expires
|
||||||
)
|
)
|
||||||
|
|
||||||
self.api_token_valid = token.generate()
|
self.api_token_valid = token.generate
|
||||||
self.hashed_token = token.token_hash(self.api_token_valid)
|
self.hashed_token = token.token_hash(self.api_token_valid)
|
||||||
token.token = self.hashed_token
|
token.token = self.hashed_token
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ class APIAuthToken(TestCase):
|
|||||||
expires = expires.strftime('%Y-%m-%d %H:%M:%S%z')
|
expires = expires.strftime('%Y-%m-%d %H:%M:%S%z')
|
||||||
|
|
||||||
|
|
||||||
self.api_token_expired = token.generate()
|
self.api_token_expired = token.generate
|
||||||
|
|
||||||
self.hashed_token_expired = token.token_hash(self.api_token_expired)
|
self.hashed_token_expired = token.token_hash(self.api_token_expired)
|
||||||
|
|
||||||
|
185
app/api/tests/unit/token/test_unit_auth_token_api_v2.py
Normal file
185
app/api/tests/unit/token/test_unit_auth_token_api_v2.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import pytest
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from django.contrib.auth.models import Permission, User
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.shortcuts import reverse
|
||||||
|
from django.test import Client, TestCase
|
||||||
|
|
||||||
|
from access.models.organization import Organization
|
||||||
|
from access.models.team import Team
|
||||||
|
from access.models.team_user import TeamUsers
|
||||||
|
|
||||||
|
from api.models.tokens import AuthToken
|
||||||
|
from api.tests.abstract.api_fields import APIModelFields
|
||||||
|
|
||||||
|
from core.models.manufacturer import Manufacturer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class API(
|
||||||
|
TestCase,
|
||||||
|
APIModelFields
|
||||||
|
):
|
||||||
|
|
||||||
|
model = AuthToken
|
||||||
|
|
||||||
|
@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.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||||
|
|
||||||
|
self.item = self.model.objects.create(
|
||||||
|
note = 'a note',
|
||||||
|
token = self.model().generate,
|
||||||
|
user = self.view_user,
|
||||||
|
expires = '2099-02-26T00:53Z'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.url_view_kwargs = {
|
||||||
|
'model_id': self.view_user.id,
|
||||||
|
'pk': self.item.id
|
||||||
|
}
|
||||||
|
|
||||||
|
view_permissions = Permission.objects.get(
|
||||||
|
codename = 'view_' + self.model._meta.model_name,
|
||||||
|
content_type = ContentType.objects.get(
|
||||||
|
app_label = self.model._meta.app_label,
|
||||||
|
model = self.model._meta.model_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
view_team = Team.objects.create(
|
||||||
|
team_name = 'view_team',
|
||||||
|
organization = self.organization,
|
||||||
|
)
|
||||||
|
|
||||||
|
view_team.permissions.set([view_permissions])
|
||||||
|
|
||||||
|
teamuser = TeamUsers.objects.create(
|
||||||
|
team = view_team,
|
||||||
|
user = self.view_user
|
||||||
|
)
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
url = reverse('v2:_api_v2_user_settings_token-detail', kwargs=self.url_view_kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
client.force_login(self.view_user)
|
||||||
|
response = client.get(url)
|
||||||
|
|
||||||
|
self.api_data = response.data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_field_exists_model_notes(self):
|
||||||
|
""" Test for existance of API Field
|
||||||
|
|
||||||
|
This test case is a duplicate of a test with the same name. This
|
||||||
|
model is not a tenancy model and does not require this field.
|
||||||
|
|
||||||
|
model_notes field must exist
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert 'model_notes' not in self.api_data
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_field_type_model_notes(self):
|
||||||
|
""" Test for type for API Field
|
||||||
|
|
||||||
|
This test case is a duplicate of a test with the same name. This
|
||||||
|
model is not a tenancy model and does not require this field.
|
||||||
|
|
||||||
|
model_notes field must be str
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert 'model_notes' not in self.api_data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_field_exists_user(self):
|
||||||
|
""" Test for existance of API Field
|
||||||
|
|
||||||
|
user field must exist
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert 'user' in self.api_data
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_field_type_user(self):
|
||||||
|
""" Test for type for API Field
|
||||||
|
|
||||||
|
normallay an object would be serialized. However in this case only
|
||||||
|
the owner of the object will access the object and therefore would
|
||||||
|
be a waste to serialize it.
|
||||||
|
|
||||||
|
user field must be int
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert type(self.api_data['user']) is int
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_field_exists_note(self):
|
||||||
|
""" Test for existance of API Field
|
||||||
|
|
||||||
|
note field must exist
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert 'note' in self.api_data
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_field_type_user(self):
|
||||||
|
""" Test for type for API Field
|
||||||
|
|
||||||
|
note field must be str
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert type(self.api_data['note']) is str
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_field_exists_expires(self):
|
||||||
|
""" Test for existance of API Field
|
||||||
|
|
||||||
|
expires field must exist
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert 'expires' in self.api_data
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_field_type_expires(self):
|
||||||
|
""" Test for type for API Field
|
||||||
|
|
||||||
|
expires field must be str
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert type(self.api_data['expires']) is str
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# def test_api_field_exists_url_history(self):
|
||||||
|
# """ Test for existance of API Field
|
||||||
|
|
||||||
|
# _urls.history field must exist
|
||||||
|
# """
|
||||||
|
|
||||||
|
# assert 'history' in self.api_data['_urls']
|
||||||
|
|
||||||
|
|
||||||
|
# def test_api_field_type_url_history(self):
|
||||||
|
# """ Test for type for API Field
|
||||||
|
|
||||||
|
# _urls.history field must be str
|
||||||
|
# """
|
||||||
|
|
||||||
|
# assert type(self.api_data['_urls']['history']) is str
|
70
app/api/tests/unit/token/test_unit_auth_token_viewset.py
Normal file
70
app/api/tests/unit/token/test_unit_auth_token_viewset.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import Client, TestCase
|
||||||
|
|
||||||
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
|
from access.models.organization import Organization
|
||||||
|
|
||||||
|
from api.tests.abstract.viewsets import ViewSetModel
|
||||||
|
|
||||||
|
from settings.viewsets.user_settings import ViewSet
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ViewsetCommon(
|
||||||
|
ViewSetModel,
|
||||||
|
):
|
||||||
|
|
||||||
|
viewset = ViewSet
|
||||||
|
|
||||||
|
route_name = 'v2:_api_v2_user_settings_token'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(self):
|
||||||
|
"""Setup Test
|
||||||
|
|
||||||
|
1. Create an organization
|
||||||
|
3. create super user
|
||||||
|
"""
|
||||||
|
|
||||||
|
organization = Organization.objects.create(name='test_org')
|
||||||
|
|
||||||
|
self.organization = organization
|
||||||
|
|
||||||
|
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
|
||||||
|
|
||||||
|
self.kwargs = {
|
||||||
|
'model_id': self.view_user.id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ViewsetList(
|
||||||
|
ViewsetCommon,
|
||||||
|
TestCase,
|
||||||
|
):
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(self):
|
||||||
|
"""Setup Test
|
||||||
|
|
||||||
|
1. make list request
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
super().setUpTestData()
|
||||||
|
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
|
||||||
|
url = reverse(
|
||||||
|
self.route_name + '-list',
|
||||||
|
kwargs = self.kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
client.force_login(self.view_user)
|
||||||
|
|
||||||
|
self.http_options_response_list = client.options(url)
|
@ -5,6 +5,7 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
|||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
from api.viewsets import (
|
from api.viewsets import (
|
||||||
|
auth_token,
|
||||||
index as v2
|
index as v2
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -224,6 +225,7 @@ router.register('settings/software_category/(?P<model_id>[0-9]+)/notes', softwar
|
|||||||
router.register('settings/ticket_category', ticket_category.ViewSet, basename='_api_v2_ticket_category')
|
router.register('settings/ticket_category', ticket_category.ViewSet, basename='_api_v2_ticket_category')
|
||||||
router.register('settings/ticket_comment_category', ticket_comment_category.ViewSet, basename='_api_v2_ticket_comment_category')
|
router.register('settings/ticket_comment_category', ticket_comment_category.ViewSet, basename='_api_v2_ticket_comment_category')
|
||||||
router.register('settings/user_settings', user_settings_v2.ViewSet, basename='_api_v2_user_settings')
|
router.register('settings/user_settings', user_settings_v2.ViewSet, basename='_api_v2_user_settings')
|
||||||
|
router.register('settings/user_settings/(?P<model_id>[0-9]+)/token', auth_token.ViewSet, basename='_api_v2_user_settings_token')
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
119
app/api/viewsets/auth_token.py
Normal file
119
app/api/viewsets/auth_token.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
|
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
|
||||||
|
|
||||||
|
from api.viewsets.common import (
|
||||||
|
ModelCreateViewSet,
|
||||||
|
ModelListRetrieveDeleteViewSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
from api.serializers.auth_token import ( # pylint: disable=W0611:unused-import
|
||||||
|
AuthToken,
|
||||||
|
AuthTokenModelSerializer,
|
||||||
|
AuthTokenViewSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_view(
|
||||||
|
create=extend_schema(
|
||||||
|
summary = 'Create an auth token',
|
||||||
|
description='',
|
||||||
|
responses = {
|
||||||
|
201: OpenApiResponse(description='Device created', response=AuthTokenViewSerializer),
|
||||||
|
400: OpenApiResponse(description='Validation failed.'),
|
||||||
|
403: OpenApiResponse(description='User is missing create permissions'),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
destroy = extend_schema(
|
||||||
|
summary = 'Delete an auth token',
|
||||||
|
description = '',
|
||||||
|
responses = {
|
||||||
|
204: OpenApiResponse(description=''),
|
||||||
|
403: OpenApiResponse(description='User is missing delete permissions'),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
list = extend_schema(
|
||||||
|
summary = 'Fetch all auth tokens',
|
||||||
|
description='',
|
||||||
|
responses = {
|
||||||
|
200: OpenApiResponse(description='', response=AuthTokenViewSerializer),
|
||||||
|
403: OpenApiResponse(description='User is missing view permissions'),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
retrieve = extend_schema(
|
||||||
|
summary = 'Fetch a single auth token',
|
||||||
|
description='',
|
||||||
|
responses = {
|
||||||
|
200: OpenApiResponse(description='', response=AuthTokenViewSerializer),
|
||||||
|
403: OpenApiResponse(description='User is missing view permissions'),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
update = extend_schema(exclude = True),
|
||||||
|
partial_update = extend_schema(exclude = True),
|
||||||
|
)
|
||||||
|
class ViewSet(
|
||||||
|
ModelCreateViewSet,
|
||||||
|
ModelListRetrieveDeleteViewSet,
|
||||||
|
):
|
||||||
|
|
||||||
|
filterset_fields = [
|
||||||
|
'expires',
|
||||||
|
]
|
||||||
|
|
||||||
|
search_fields = [
|
||||||
|
'note',
|
||||||
|
]
|
||||||
|
|
||||||
|
model = AuthToken
|
||||||
|
|
||||||
|
view_description = 'User Authentication Tokens'
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
|
||||||
|
if self.serializer_class is not None:
|
||||||
|
|
||||||
|
return self.serializer_class
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.action == 'list'
|
||||||
|
or self.action == 'retrieve'
|
||||||
|
):
|
||||||
|
|
||||||
|
self.serializer_class = globals()[str( self.model._meta.verbose_name).replace(' ' , '') + 'ViewSerializer']
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
self.serializer_class = globals()[str( self.model._meta.verbose_name).replace(' ' , '') + 'ModelSerializer']
|
||||||
|
|
||||||
|
|
||||||
|
return self.serializer_class
|
||||||
|
|
||||||
|
|
||||||
|
def get_back_url(self) -> str:
|
||||||
|
|
||||||
|
if self.back_url is None:
|
||||||
|
|
||||||
|
self.back_url = self.get_return_url()
|
||||||
|
|
||||||
|
|
||||||
|
return self.back_url
|
||||||
|
|
||||||
|
|
||||||
|
def get_return_url(self) -> str:
|
||||||
|
|
||||||
|
if getattr(self, '_get_return_url', None):
|
||||||
|
|
||||||
|
return self._get_return_url
|
||||||
|
|
||||||
|
self._get_return_url = reverse(
|
||||||
|
'v2:_api_v2_user_settings-detail',
|
||||||
|
kwargs = {
|
||||||
|
'pk': self.kwargs['model_id']
|
||||||
|
},
|
||||||
|
request = self.request,
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._get_return_url
|
||||||
|
|
@ -96,6 +96,28 @@ class UserSettings(UserSettingsCommonFields):
|
|||||||
verbose_name = 'Your Timezone',
|
verbose_name = 'Your Timezone',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
page_layout: list = [
|
||||||
|
{
|
||||||
|
"name": "Details",
|
||||||
|
"slug": "details",
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"layout": "single",
|
||||||
|
"fields": [
|
||||||
|
'browser_mode',
|
||||||
|
'default_organization',
|
||||||
|
'timezone',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Auth Tokens",
|
||||||
|
"layout": "table",
|
||||||
|
"field": "tokens",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_organization(self):
|
def get_organization(self):
|
||||||
|
|
||||||
|
@ -48,6 +48,13 @@ class UserSettingsModelSerializer(UserSettingsBaseSerializer):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'_self': reverse("v2:_api_v2_user_settings-detail", request=self._context['view'].request, kwargs={'pk': item.pk}),
|
'_self': reverse("v2:_api_v2_user_settings-detail", request=self._context['view'].request, kwargs={'pk': item.pk}),
|
||||||
|
'tokens': reverse(
|
||||||
|
"v2:_api_v2_user_settings_token-list",
|
||||||
|
request=self._context['view'].request,
|
||||||
|
kwargs={
|
||||||
|
'model_id': item.user.pk
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -127,6 +127,7 @@ RUN pip --disable-pip-version-check list --outdated --format=json | \
|
|||||||
python -c "import json, sys; print('\n'.join([x['name'] for x in json.load(sys.stdin)]))" | \
|
python -c "import json, sys; print('\n'.join([x['name'] for x in json.load(sys.stdin)]))" | \
|
||||||
xargs -n1 pip install --no-cache-dir -U; \
|
xargs -n1 pip install --no-cache-dir -U; \
|
||||||
apk update --no-cache; \
|
apk update --no-cache; \
|
||||||
|
apk upgrade --no-cache; \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
mariadb-client \
|
mariadb-client \
|
||||||
mariadb-dev \
|
mariadb-dev \
|
||||||
|
@ -30,14 +30,6 @@ Models are tested using the following test cases:
|
|||||||
|
|
||||||
- [View Permission (organization Manager)](./model_permission_view_organization_manager.md)
|
- [View Permission (organization Manager)](./model_permission_view_organization_manager.md)
|
||||||
|
|
||||||
- [History Entry](./model_history.md)
|
|
||||||
|
|
||||||
- [History Entry (Child Item)](./model_history_child_item.md)
|
|
||||||
|
|
||||||
- [History Entry (Parent Item)](./model_history_parent_item.md)
|
|
||||||
|
|
||||||
- [History Entry Permissions](./model_history_permissions.md)
|
|
||||||
|
|
||||||
- [Model Views](./model_views.md)
|
- [Model Views](./model_views.md)
|
||||||
|
|
||||||
- [Tenancy Objects](./model_tenancy_object.md)
|
- [Tenancy Objects](./model_tenancy_object.md)
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
title: History Entry Test Cases
|
|
||||||
description: No Fuss Computings model history entry unit test cases
|
|
||||||
date: 2024-06-15
|
|
||||||
template: project.html
|
|
||||||
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp
|
|
||||||
---
|
|
||||||
|
|
||||||
::: app.core.tests.abstract.history_entry.HistoryEntry
|
|
@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
title: Child Item History Entry Test Cases
|
|
||||||
description: No Fuss Computings model child item history entry unit test cases
|
|
||||||
date: 2024-06-15
|
|
||||||
template: project.html
|
|
||||||
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp
|
|
||||||
---
|
|
||||||
|
|
||||||
::: app.core.tests.abstract.history_entry_child_model.HistoryEntryChildItem
|
|
@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
title: Parent Item History Entry Test Cases
|
|
||||||
description: No Fuss Computings model parent item history entry unit test cases
|
|
||||||
date: 2024-06-15
|
|
||||||
template: project.html
|
|
||||||
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp
|
|
||||||
---
|
|
||||||
|
|
||||||
::: app.core.tests.abstract.history_entry_parent_model.HistoryEntryParentItem
|
|
@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
title: History Entry Permission Test Cases
|
|
||||||
description: No Fuss Computings model history entry permission unit test cases
|
|
||||||
date: 2024-06-16
|
|
||||||
template: project.html
|
|
||||||
about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp
|
|
||||||
---
|
|
||||||
|
|
||||||
::: app.core.tests.abstract.history_permissions.HistoryPermissions
|
|
||||||
options:
|
|
||||||
show_source: true
|
|
@ -128,7 +128,7 @@ Items to test include, and are not limited to:
|
|||||||
|
|
||||||
- can access global object (still to require model CRUD permission)
|
- can access global object (still to require model CRUD permission)
|
||||||
|
|
||||||
- history - [History Entries](./api/tests/model_history.md), [History Permissions](./api/tests/model_history_permissions.md)
|
- history
|
||||||
|
|
||||||
- saves history with parent pk and parent class
|
- saves history with parent pk and parent class
|
||||||
|
|
||||||
|
Submodule gitlab-ci updated: 6f8dfcba0b...034a153ba0
@ -109,14 +109,6 @@ nav:
|
|||||||
|
|
||||||
- projects/centurion_erp/development/api/tests/models.md
|
- projects/centurion_erp/development/api/tests/models.md
|
||||||
|
|
||||||
- projects/centurion_erp/development/api/tests/model_history.md
|
|
||||||
|
|
||||||
- projects/centurion_erp/development/api/tests/model_history_child_item.md
|
|
||||||
|
|
||||||
- projects/centurion_erp/development/api/tests/model_history_parent_item.md
|
|
||||||
|
|
||||||
- projects/centurion_erp/development/api/tests/model_history_permissions.md
|
|
||||||
|
|
||||||
- projects/centurion_erp/development/api/tests/model_permissions.md
|
- projects/centurion_erp/development/api/tests/model_permissions.md
|
||||||
|
|
||||||
- projects/centurion_erp/development/api/tests/model_permissions_organization_manager.md
|
- projects/centurion_erp/development/api/tests/model_permissions_organization_manager.md
|
||||||
|
Reference in New Issue
Block a user